Monitoring iBeacons without an iOS device

As I already said in my previous post on iBeacons, you don’t need an iOS device to monitor iBeacons. Sure, iOS comes with inbuilt support for monitoring and ranging beacons, but since beacons are plain Bluetooth LE devices that send out a periodic advertisement packet, all you need is something that can find Bluetooth LE devices. A compatible Android device, or even a simple bluetooth LE adapter like ConnectBlue OBS 421 will do just fine.

Lets talk about how we can find bluetooth devices using Android. Android 4.3 (API Level 18) introduces built-in platform support for Bluetooth Low Energy in the central role and provides APIs that apps can use to discover devices, query for services, and read/write characteristics.

Just declare the necessary permissions in your manifest file.

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

If your app heavily on iBeacons and you want to make it available to only BLE-capable devices, include this as well:

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

Or, you can set it to false and then check if BLE is available.

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish();
}

To find the BLE devices, you would need to get the BluetoothAdapter, enable bluetooth and then start a scan.

// Initializes Bluetooth adapter.
final BluetoothManager bluetoothManager =
        (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();

// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
private void scanLeDevice(final boolean enable) {
    if (enable) {
        // Stops scanning after a pre-defined scan period.
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mScanning = false;
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            }
        }, SCAN_PERIOD);

        mScanning = true;
        mBluetoothAdapter.startLeScan(mLeScanCallback);
    } else {
        mScanning = false;
        mBluetoothAdapter.stopLeScan(mLeScanCallback);
    }
    ...
}

As with iOS, you can filter the devices with UUID by using startLeScan (UUID[], BluetoothAdapter.LeScanCallback). Here’s an implementation of the callback.

// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        runOnUiThread(new Runnable() {
           @Override
           public void run() {
               mLeDeviceListAdapter.addDevice(device);
               mLeDeviceListAdapter.notifyDataSetChanged();
           }
       });
   }
};

This is where it starts to get interesting. Since, Android dosen’t (yet) support the iBeacon specification, you need to manually parse the advertisement packet (scanData) to find the details about the scanned beacon. From StackOverflow,

For an iBeacon with ProximityUUID E2C56DB5-DFFB-48D2-B060-D0F5A71096E0, major 0, minor 0, and calibrated Tx Power of -59 RSSI, the transmitted BLE advertisement packet looks like this:

d6 be 89 8e 40 24 05 a2 17 6e 3d 71 02 01 1a 1a ff 4c 00 02 15 e2 c5 6d b5 df fb 48 d2 b0 60 d0 f5 a7 10 96 e0 00 00 00 00 c5 52 ab 8d 38 a5

This packet can be broken down as follows:

d6 be 89 8e # Access address for advertising data (this is always the same fixed value)
40 # Advertising Channel PDU Header byte 0.  Contains: (type = 0), (tx add = 1), (rx add = 0)
24 # Advertising Channel PDU Header byte 1.  Contains:  (length = total bytes of the advertising payload + 6 bytes for the BLE mac address.)
05 a2 17 6e 3d 71 # Bluetooth Mac address (note this is a spoofed address)
02 01 1a 1a ff 4c 00 02 15 e2 c5 6d b5 df fb 48 d2 b0 60 d0 f5 a7 10 96 e0 00 00 00 00 c5 # Bluetooth advertisement
52 ab 8d 38 a5 # checksum

So, all you need is to read the appropriate subarrays from scanData and use new String(byte[], "UTF-8") to convert it to String.

Finding the distance (or accuracy as it is called) is a bit more involved. First thing that you will realize when managing beacons this way is that you cannot rely on just one measurement of RSSI. You need to keep a running average of RSSI values to get a reliable estimate and even that is not going to be super accurate and there’s not much you can do about it.