iBeacon detection with Android

In this post, I will introduce you to Bluetooth Low Energy technology, iBeacon, and how to make a simple app in Android to detect iBeacon

Why this post

To start with, Android OS doesn’t have a “native” iBeacon detection in place, but it does support scanning BLE devices, which are a lower-level device than iBeacon

For that reason, there are a few libraries that are developed for detecting iBeacon on Android, which are very easy to integrate. You can also look for other sample applications, and see how they work by looking at their source code since almost all of them are open-sourced.

The problem is that I cannot find any tutorial/sample application that describes in detail how they process BLE packet, identify iBeacon and extracting data from packet. Of course, the source-code is available on the internet, but having a tutorial that explains in detail would be better, hence the existence of this post.

There are a few things to note:

  • Since I’m not an embedded engineer, I will not go into details about BLE and iBeacon specifications, but I will note some references at the end in case you want to know more about them.
  • I assume that you have already known the basics of Android development, so I will only focus on the handling logic. You can decide how to display scanning results on UI by yourself.
  • I only started learning Android development recently, so the code below may not be best practice, especially permission checking logic. Feel free to share your comment by sending me a message or creating new PR on Github (I will mention Github repo URL at the end)

Bluetooth Low Energy

Bluetooth Low Energy (BLE) is a wireless personal area network technology designed and marketed by the Bluetooth Special Interest Group (Bluetooth SIG). It was integrated into Bluetooth 4.0 in 2009. BLE is based on Bluetooth, aimed to provide many of the same features as Bluetooth but using less power (hence the name)

Why using BLE

  • low power requirement (communicate in short bursts, when not connected it remains in sleep mode)
  • lower latency
  • small size (physically)
  • compatibility with mobile phones, tablets, and computers (since 2012, almost all smartphones support BLE)

* Note that BLE has lower data transfer rate compared to Bluetooth, so it is not suitable for transferring large files

iBeacon

iBeacon is a protocol developed by Apple, which allows electronic devices (mostly smartphone, tablet) to listen for signals from beacons and perform action correspondingly when in proximity to an iBeacon.

iBeacon is based on Bluetooth low energy proximity sensing, emitting advertisement in a standard format to notify nearby devices of their presence.

You can download the official iBeacon specification from Apple here by selecting [Download Artwork and Specifications] https://developer.apple.com/ibeacon/

iBeacon packet structure

The standard format of iBeacon packet is as below (taken from Apple specification)

Bytes Name Value Notes
0 Flags[0] 0x02
1 Flags[1] 0x01
2 Flags[2] 0x06
3 Length 0x1A
4 Type 0xFF
5 Company ID[0] 0x4C
6 Company ID[1] 0x00
7 Beacon Type[0] 0x02 Must be set to 0x02 for all Proximity Beacons
8 Beacon Type[1] 0x15 Must be set to 0x15 for all Proximity Beacons
9-24 Proximity UUID 0xnn..nn
25-26 Major 0xnnnn
27-28 Minor 0xnnnn
29 Measured Power 0xnn

You can take a look at Apple document if you want to understand more about iBeacon. For now I will focus on how to detect iBeacon and parsing packet, then display it in the app

Detecting iBeacon on Android (Kotlin)

Permission

In order to detect iBeacon on Android, app needs Location and Bluetooth permission.

  • On Android, BLUETOOTH and BLUETOOTH_ADMIN are normal permission, they are automatically granted. You don’t have to ask for user permission.
  • However, from Android 6.0, location permission needs to be enabled for Bluetooth Low Energy Scanning. This by design, not a bug, since developer can scan for Bluetooth beacons and figure out user location, location permission is necessary.
  • So on Android version <12, you need to ask for ACCESS_FINE_LOCATION permission
  • From Android 12, you can use the new BLUETOOTH_SCAN and BLUETOOTH_CONNECT to scan for BLE devices
  • These are runtime permissions, so we must ask for user permission explicitly.

First we need to declare permission that app needs to detect iBeacon. Open AndroidManifest.xml and add the following to the manifest file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission
    android:name="android.permission.ACCESS_COARSE_LOCATION"
    android:maxSdkVersion="30" />
<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="30" />

Then we need to request permission when app is started:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// necessary permissions on Android <12
private val BLE_PERMISSIONS = arrayOf(
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.ACCESS_FINE_LOCATION
)

// necessary permissions on Android >=12
private val ANDROID_12_BLE_PERMISSIONS = arrayOf(
    Manifest.permission.BLUETOOTH_SCAN,
    Manifest.permission.BLUETOOTH_CONNECT,
    Manifest.permission.ACCESS_FINE_LOCATION
)

/**
 * Determine whether the location permission has been granted
 * if not, request the permission
 *
 * @param context
 * @return true if user has granted permission
 */
private fun isPermissionGranted(context: Context): Boolean {
    Log.d(TAG, "@isPermissionGranted: checking bluetooth")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        if ((ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.BLUETOOTH_CONNECT
            ) != PackageManager.PERMISSION_GRANTED) ||
            (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.BLUETOOTH_SCAN
            ) != PackageManager.PERMISSION_GRANTED)
        ) {
            Log.d(TAG, "@isPermissionGranted: requesting Bluetooth on Android >= 12")
            ActivityCompat.requestPermissions(this, ANDROID_12_BLE_PERMISSIONS, 2)
            return false
        }
    } else {
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            Log.d(TAG, "@isPermissionGranted: requesting Location on Android < 12")
            ActivityCompat.requestPermissions(this, BLE_PERMISSIONS, 3)
            return false
        }
    }
    Log.d(TAG, "@isPermissionGranted Bluetooth permission is ON")
    return true
}

And call these when your Activity is initialized:

1
2
3
4
// check for permission to scan BLE
if (isPermissionGranted(this)) {
    // start scan service here
}

Setting up Bluetooth

I will create a separated class ScanService for handling Bluetooth device detection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ScanService {

    private val bluetoothManager: BluetoothManager
    private val bluetoothAdapter: BluetoothAdapter
    private val bluetoothLeScanner: BluetoothLeScanner

    private val TAG = "ScanService"

    private var isScanning = false
    private val handler = Handler()

    // Stops scanning after 10 seconds.
    private var SCAN_PERIOD: Long = 10000

    constructor(context: Context,) {
        // Initialize logic goes here
    }

    fun isScanning(): Boolean {
        return isScanning
    }

}

To perform any activities related to Bluetooth, we need a BluetoothAdapter which represents device’s Bluetooth adapter. We can get it from BluetoothManager.

Then from BluetoothAdapter, we can check if Bluetooth is enabled. If yes, get bluetoothLeScanner from adapter. This BluetoothLeScanner class provides methods to perform scan-related operations for BLE devices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
constructor(context: Context,) {
    bluetoothManager = context.getSystemService(BluetoothManager::class.java)
    bluetoothAdapter = bluetoothManager.adapter
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
        throw Exception("Device doesn't support Bluetooth")
    }
    bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
}

/**
 * Determine whether bluetooth is enabled or not
 *
 * @return true if bluetooth is enabled, false otherwise
 */
fun isBluetoothEnabled() : Boolean {
    return bluetoothAdapter.isEnabled
}

Create 2 methods for starting and stopping scan process:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Start BLE scan using bluetoothLeScanner
 * if app is not scanning, start scan by calling startScan and passing a callback method
 * else stop scan
 * @return {none}
 */
fun startBLEScan() {
    try {
        if (!isScanning) {
            handler.postDelayed({
                isScanning = false
                bluetoothLeScanner.stopScan(leScanCallback)
            }, SCAN_PERIOD)
            isScanning = true

            bluetoothLeScanner.startScan(leScanCallback)
        } else {
            isScanning = false
            bluetoothLeScanner.stopScan(leScanCallback)
        }
    } catch (e: SecurityException) {
        Log.e(TAG, "@startScan SecurityException: " + e.message)
    }
}

fun stopBLEScan() {
    if (isScanning) {
        bluetoothLeScanner.stopScan(leScanCallback)
    }
}

Implementation of ScanCallback, scan result will be delivered in this callback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * callback method when app detects BLE device
 */
private val leScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        val scanRecord = result?.scanRecord
        super.onScanResult(callbackType, result)
        // logic goes here
        return
    }
}

We have detected BLE devices, now we need to check which one of them is iBeacon. AFAIK the only way to do this is by comparing the packet pattern if they matches iBeacon pattern or not (if there is another way to do it, please let me know).

Let’s look again at iBeacon packet format:

  • The first 9 bytes are called prefix, which are specified by the vendor.
  • Next we have iBeacon 4 main information: UUID, Major, Minor, Power.

Since there four (UUID, Major, Minor, Power) are varied by each beacon, we cannot use them to judge if BLE device is iBeacon or not.

In prefix bytes, from 0 to 4 is specified by Bluetooth 4.0, so we can ignore them.

We just need to check from byte 5 to 8 if they match Apple specified value.

Create a static class BLEDevice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
object BLEDevice {

    /**
     * Check if packet is from an iBeacon
     * @param packetData packet data which app captured
     * @return true if packet is from iBeacon, otherwise false
     */
    fun isIBeacon(packetData: ByteArray): Boolean {
        var startByte = 2
        while (startByte <= 5) {
            if (packetData[startByte + 2].toInt() and 0xff == 0x02 && packetData[startByte + 3].toInt() and 0xff == 0x15) {
                return true
            }
            startByte++
        }
        return false
    }

    /**
     * convert bytes data to hex string
     * @param bytes input bytes
     * @return hex string
     */
    fun bytesToHex(bytes: ByteArray): String {
        val hexArray = "0123456789ABCDEF".toCharArray()
        val hexChars = CharArray(bytes.size * 2)
        for (j in bytes.indices) {
            val v: Int = bytes[j].toInt() and 0xFF
            hexChars[j * 2] = hexArray.get(v ushr 4)
            hexChars[j * 2 + 1] = hexArray.get(v and 0x0F)
        }
        return String(hexChars)
    }
}

We will use isIBeacon to determine if the received packet is from an iBeacon or not.

Handling iBeacon detection

We will create IBeacon class to store beacon information:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class IBeacon {

    /**
     * beacon UUID
     */
    private var uuid: String = ""

    /**
     * The measured signal strength of the Bluetooth packet
     */
    private var rssi: Int = 0

    /**
     * packet raw data
     */
    private var rawByteData: ByteArray = ByteArray(30)

    /**
     * major minor, and their position (based on iBeacon specs)
     */
    private var major: Int? = null
    private val majorPosStart = 25
    private val majorPosEnd = 26

    private var minor: Int? = null
    private val minorPosStart = 27
    private val minorPosEnd = 28

    private val TAG = "IBeacon"

    constructor(packetData: ByteArray) {
        rawByteData = packetData
    }

    /**
     * Parse iBeacon UUID from packet
     */
    private fun parseUUID() {
        // UUID parsing logic goes here
    }

    fun getUUID(): String {
    }

    fun getMajor(): Int {
    }

    fun getMinor(): Int {
    }
}

Parsing UUID, major, minor

in parseUUID, we will convert UUID in packet data to readable string. UUID has 16 bytes, from 9 to 24:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private fun parseUUID() {
    var startByte = 2
    while (startByte <= 5) {
        if (rawByteData[startByte + 2].toInt() and 0xff == 0x02 && rawByteData[startByte + 3].toInt() and 0xff == 0x15) {
            val uuidBytes = ByteArray(16)
            System.arraycopy(rawByteData, startByte + 4, uuidBytes, 0, 16)
            val hexString = BLEDevice.bytesToHex(uuidBytes)
            if (!hexString.isNullOrEmpty()) {
                uuid = hexString.substring(0, 8) + "-" +
                        hexString.substring(8, 12) + "-" +
                        hexString.substring(12, 16) + "-" +
                        hexString.substring(16, 20) + "-" +
                        hexString.substring(20, 32)
                return
            }
        }
        startByte++
    }
}

/**
 * UUID getter method
 * if UUID is not calculated, calculate from packet raw data, then store to property
 */
fun getUUID(): String {
    if (uuid.isNullOrEmpty()) {
        parseUUID()
    }
    return uuid
}

Similar to UUID, next is major (byte no 25-26) and minor (byte no 27-28):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * Get iBeacon major
 * if major is not calculated, calculate from packet raw data, then store to property
 */
fun getMajor(): Int {
    if (major == null)
        major = (rawByteData[majorPosStart].toInt() and 0xff) * 0x100 + (rawByteData[majorPosEnd].toInt() and 0xff)
    return major as Int
}

/**
 * Get iBeacon minor
 * if minor is not calculated, calculate from packet raw data, then store to property
 */
fun getMinor(): Int {
    if (minor == null)
        minor = (rawByteData[minorPosStart].toInt() and 0xff) * 0x100 + (rawByteData[minorPosEnd].toInt() and 0xff)
    return minor as Int
}

Now we will integrate all those logic above into ScanCallback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * callback method when app detects BLE device
 * parse packet then print to log
 */
override fun onScanResult(callbackType: Int, result: ScanResult?) {
    val scanRecord = result?.scanRecord
    super.onScanResult(callbackType, result)
    try {
        if (scanRecord != null && BLEDevice.isIBeacon(scanRecord?.bytes)) {
            Log.d(TAG, "Device is iBeacon")
            val device = IBeacon(
                scanRecord?.bytes
            )
            Log.e(TAG, "@startScan " + "Device UUID: " + device.getUUID() + "\n" + "Major: " + device.getMajor() + "\n" + "Minor: " + device.getMinor() + "\n")
        }
    } catch (e: SecurityException) {
        Log.e(TAG, "@startScan SecurityException: " + e.message)
    }
    return
}

The logic is really simple:

  • When app received callback from BLE device, we will check if device is iBeacon or not with isIBeacon method
  • If the device is iBeacon, then initialize new IBeacon object
  • Print UUID, major, minor to log

Now run the app on your testing device, turn on iBeacon and your app should be able to detect it:

1
2
3
Device UUID: C41D6798-B34B-40B7-8362-631DFB080D71
Major: 2
Minor: 1825

That is all for basic iBeacon detection on Android. You can base on this logic to create your own iBeacon detection app.

If you want to see the sample code, here is the repository: Simple BLE Detection

References:

Apple iBeacon specification

Android Developer - Bluetooth Low Energy

Understanding the different types of BLE Beacons

StackOverflow - How to detect iBeacon in Android?

StackOverflow - How to detect IBeacon in android without using any library

StackOverflow - Does an iBeacon have to use Apple&rsquo;s company ID? If not, how to identify an iBeacon?

StackOverflow - parse the following ibeacon packet

Requesting multiple Bluetooth permissions in Android Marshmallow

Android Beacon Library