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.
// necessary permissions on Android <12
privatevalBLE_PERMISSIONS=arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_FINE_LOCATION)// necessary permissions on Android >=12
privatevalANDROID_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
*/privatefunisPermissionGranted(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)returnfalse}}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)returnfalse}}Log.d(TAG,"@isPermissionGranted Bluetooth permission is ON")returntrue}
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:
classScanService{privatevalbluetoothManager:BluetoothManagerprivatevalbluetoothAdapter:BluetoothAdapterprivatevalbluetoothLeScanner:BluetoothLeScannerprivatevalTAG="ScanService"privatevarisScanning=falseprivatevalhandler=Handler()// Stops scanning after 10 seconds.
privatevarSCAN_PERIOD:Long=10000constructor(context:Context,){// Initialize logic goes here
}funisScanning():Boolean{returnisScanning}}
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.adapterif(bluetoothAdapter==null){// Device doesn't support Bluetooth
throwException("Device doesn't support Bluetooth")}bluetoothLeScanner=bluetoothAdapter.bluetoothLeScanner}/**
* Determine whether bluetooth is enabled or not
*
* @return true if bluetooth is enabled, false otherwise
*/funisBluetoothEnabled():Boolean{returnbluetoothAdapter.isEnabled}
Create 2 methods for starting and stopping scan process:
/**
* 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}
*/funstartBLEScan(){try{if(!isScanning){handler.postDelayed({isScanning=falsebluetoothLeScanner.stopScan(leScanCallback)},SCAN_PERIOD)isScanning=truebluetoothLeScanner.startScan(leScanCallback)}else{isScanning=falsebluetoothLeScanner.stopScan(leScanCallback)}}catch(e:SecurityException){Log.e(TAG,"@startScan SecurityException: "+e.message)}}funstopBLEScan(){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
*/privatevalleScanCallback=object: ScanCallback(){overridefunonScanResult(callbackType:Int,result:ScanResult?){valscanRecord=result?.scanRecordsuper.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.
objectBLEDevice{/**
* Check if packet is from an iBeacon
* @param packetData packet data which app captured
* @return true if packet is from iBeacon, otherwise false
*/funisIBeacon(packetData:ByteArray):Boolean{varstartByte=2while(startByte<=5){if(packetData[startByte+2].toInt()and0xff==0x02&&packetData[startByte+3].toInt()and0xff==0x15){returntrue}startByte++}returnfalse}/**
* convert bytes data to hex string
* @param bytes input bytes
* @return hex string
*/funbytesToHex(bytes:ByteArray):String{valhexArray="0123456789ABCDEF".toCharArray()valhexChars=CharArray(bytes.size*2)for(jinbytes.indices){valv:Int=bytes[j].toInt()and0xFFhexChars[j*2]=hexArray.get(vushr4)hexChars[j*2+1]=hexArray.get(vand0x0F)}returnString(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:
classIBeacon{/**
* beacon UUID
*/privatevaruuid:String=""/**
* The measured signal strength of the Bluetooth packet
*/privatevarrssi:Int=0/**
* packet raw data
*/privatevarrawByteData:ByteArray=ByteArray(30)/**
* major minor, and their position (based on iBeacon specs)
*/privatevarmajor:Int?=nullprivatevalmajorPosStart=25privatevalmajorPosEnd=26privatevarminor:Int?=nullprivatevalminorPosStart=27privatevalminorPosEnd=28privatevalTAG="IBeacon"constructor(packetData:ByteArray){rawByteData=packetData}/**
* Parse iBeacon UUID from packet
*/privatefunparseUUID(){// UUID parsing logic goes here
}fungetUUID():String{}fungetMajor():Int{}fungetMinor():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:
privatefunparseUUID(){varstartByte=2while(startByte<=5){if(rawByteData[startByte+2].toInt()and0xff==0x02&&rawByteData[startByte+3].toInt()and0xff==0x15){valuuidBytes=ByteArray(16)System.arraycopy(rawByteData,startByte+4,uuidBytes,0,16)valhexString=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
*/fungetUUID():String{if(uuid.isNullOrEmpty()){parseUUID()}returnuuid}
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
*/fungetMajor():Int{if(major==null)major=(rawByteData[majorPosStart].toInt()and0xff)*0x100+(rawByteData[majorPosEnd].toInt()and0xff)returnmajorasInt}/**
* Get iBeacon minor
* if minor is not calculated, calculate from packet raw data, then store to property
*/fungetMinor():Int{if(minor==null)minor=(rawByteData[minorPosStart].toInt()and0xff)*0x100+(rawByteData[minorPosEnd].toInt()and0xff)returnminorasInt}
Now we will integrate all those logic above into ScanCallback: