iBeacon detection with Android (2): Create UI and improve logic

In last post, I demonstrated how to detect iBeacon on Android by capturing BLE packet and checking packet structure. This time, I will add some improvements to our logic and display data on UI:

  • User should be able to start/stop scan from UI
  • Scanned devices are displayed in a list
  • If the device is iBeacon, display iBeacon exclusive properties
  • If the device is not iBeacon, display BLE general properties
  • If app receives packet from a new device, add to list
  • If app receives packet from the same device, only update list

UI

Main view

I will use Recycleview to display BLE device list. The code below is used in activity_main.xml:

 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
<LinearLayout
    android:id="@+id/linearLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="1dp"
    android:layout_marginTop="1dp"
    android:layout_marginEnd="1dp"
    android:layout_marginBottom="1dp"
    android:orientation="vertical"
    android:background="@color/White"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:gravity="center"
    android:weightSum="10">

    <Button
        android:id="@+id/scanBtn"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:text="@string/label_scan"
        android:textColor="@color/White"
        android:backgroundTint="@color/MediumSeaGreen"
        />
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/deviceList"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="9"/>

    <Button
        android:id="@+id/exitBtn"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:text="@string/label_exit"
        android:layout_marginTop="30dp"
        android:textColor="@color/White"
        android:backgroundTint="@color/Crimson"
        />
</LinearLayout>

Then in MainAcitivy, I will handle events for Start button and Exit button. The logic for start scanning service is simple:

  • check if Bluetooth is enabled or not
  • if Bluetooth is enabled, check scanning flag in ScanService
  • if app is scanning, stop scan, else start scan
  • if Bluetooth is disabled, request user to turn on Bluetooth
 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
/**
 * Start BLE scan
 * Check Bluetooth before scanning.
 * If Bluetooth is disabled, request user to turn on Bluetooth
 */
private fun startScan() {
    // check Bluetooth
    if (!scanService.isBluetoothEnabled()) {
        Log.d(TAG, "@startScan Bluetooth is disabled")
        val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        requestBluetooth.launch(intent)
    } else {

        scanService.initScanner()
        // start scanning BLE device
        if (scanService.isScanning()) {
            binding.scanBtn.text = resources.getString(R.string.label_scan)
            scanService.stopBLEScan()
        } else {
            scanService.startBLEScan()
            binding.scanBtn.text = resources.getString(R.string.label_scanning)
        }
    }
}

/**
 * exit application
 */
private fun exitApp() {
    // if scanning service is running, stop scan then exit
    if (scanService.isScanning()) {
        binding.scanBtn.text = resources.getString(R.string.label_scan)
        scanService.stopBLEScan()
    }
    this@MainActivity.finish()
    exitProcess(0)
}

private var requestBluetooth =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
            Log.d(TAG, "@requestBluetooth Bluetooth is enabled")
        } else {
            Log.d(TAG, "@requestBluetooth Bluetooth usage is denied")
        }
    }

Item list UI

Since I need to display BLE device and iBeacon in the same list, and their data are different, I need to create 2 different layouts for them, then populate the view based on their type.

ble_item.xml for displaying BLE device:

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <TextView
        android:id="@+id/text_ble"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="@string/label_BLE_device"
        android:textSize="20sp"
        android:textColor="@color/Black" />
    <TextView
        android:id="@+id/text_address"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_ble"
        android:layout_marginTop="5dp"
        android:text="@string/label_address"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_address_value"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_ble"
        android:layout_toRightOf="@+id/text_address"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_name"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_address"
        android:layout_marginTop="5dp"
        android:text="@string/label_name"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_name_value"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_address"
        android:layout_toRightOf="@+id/text_name"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_rssi"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_name"
        android:layout_marginTop="5dp"
        android:text="@string/label_rssi"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_rssi_value"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_name"
        android:layout_toRightOf="@+id/text_rssi"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />

</RelativeLayout>

ibeacon_item.xml for displaying iBeacon:

  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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <TextView
        android:id="@+id/text_ibeacon"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="@string/label_ibeacon"
        android:textSize="20sp"
        android:textColor="@color/Black" />
    <TextView
        android:id="@+id/text_address"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_ibeacon"
        android:layout_marginTop="5dp"
        android:text="@string/label_address"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_address_value"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_ibeacon"
        android:layout_toRightOf="@+id/text_address"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_uuid"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_address"
        android:layout_marginTop="5dp"
        android:text="@string/label_uuid"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_uuid_value"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_address"
        android:layout_toRightOf="@+id/text_uuid"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_major"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_uuid"
        android:layout_marginTop="5dp"
        android:text="@string/label_major"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_major_value"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_major"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_minor"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_uuid"
        android:layout_toRightOf="@+id/text_major"
        android:layout_marginTop="5dp"
        android:text="@string/label_minor"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_minor_value"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_minor"
        android:layout_toRightOf="@+id/text_major"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_rssi"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_uuid"
        android:layout_toRightOf="@+id/text_minor"
        android:layout_marginTop="5dp"
        android:text="@string/label_rssi"
        android:textStyle="bold"
        android:textColor="@android:color/black" />
    <TextView
        android:id="@+id/text_rssi_value"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_below="@+id/text_rssi"
        android:layout_toRightOf="@+id/text_minor"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/black" />

</RelativeLayout>

Next is creating a new adapter called DeviceListAdapter to bind the data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class DeviceListAdapter(private val deviceList: ArrayList<Any>) : Adapter<RecyclerView.ViewHolder>() {

    override fun getItemViewType(position: Int): Int {
    }

    // Create new views (invoked by the layout manager)
    override fun onCreateViewHolder(viewGroup: ViewGroup,  viewType: Int): RecyclerView.ViewHolder {

    }

    // Replace the contents of a view (invoked by the layout manager)
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

    }

    override fun getItemCount(): Int {
        return deviceList.size
    }
}

In this adapter, I need to do the below:

  • Override getItemViewType to check what kind of device, is it iBeacon or just BLE
  • in onCreateViewHolder, return different ViewHolders based on the ViewType
  • Populate corresponding data in onBindViewHolder

First I need to define constant for Viewtype outside of DeviceListAdapter class (to follow Kotlin style guide)

1
2
const val VIEW_TYPE_BLE = 0
const val VIEW_TYPE_IBEACON = 1

Now inside DeviceListAdapter, create 2 ViewHolder for BLE and iBeacon. Depends on the type, we need to set different data:

 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
class BLEViewHolder(view: View): RecyclerView.ViewHolder(view) {
    val address: TextView
    val rssi: TextView

    init {
        address = view.findViewById(R.id.text_address_value)
        rssi = view.findViewById(R.id.text_rssi_value)
    }
}

class IBeaconViewHolder(view: View): RecyclerView.ViewHolder(view) {
    val uuid: TextView
    val major: TextView
    val minor: TextView
    val address: TextView
    val rssi: TextView

    init {
        uuid = view.findViewById(R.id.text_uuid_value)
        major = view.findViewById(R.id.text_major_value)
        minor = view.findViewById(R.id.text_minor_value)
        address = view.findViewById(R.id.text_address_value)
        rssi = view.findViewById(R.id.text_rssi_value)
    }
}

In getItemViewType, check if current object is BLE or iBeacon, then return corresponding viewtype

Then in onCreateViewHolder, return ViewHolder based on viewtype.

Finally, populate data in onBindViewHolder

 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
/**
 * Get viewtype based on BLE device type
 * If BLE device is iBeacon, use iBeacon layout
 */
override fun getItemViewType(position: Int): Int {
    val dev: Any = deviceList.get(position)
    if (dev is IBeacon)
        return VIEW_TYPE_IBEACON
    return VIEW_TYPE_BLE
}

// Create new views (invoked by the layout manager)
override fun onCreateViewHolder(viewGroup: ViewGroup,  viewType: Int): RecyclerView.ViewHolder {
    // Create a new view, which defines the UI of the list item
    if (viewType == VIEW_TYPE_IBEACON) {
        return IBeaconViewHolder(LayoutInflater.from(viewGroup.context).inflate(R.layout.ibeacon_item, viewGroup,false))
    }
    return BLEViewHolder(LayoutInflater.from(viewGroup.context).inflate(R.layout.ble_item, viewGroup,false))

}

// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when (holder.itemViewType) {
        VIEW_TYPE_IBEACON -> {
            val iBeaconView = holder as IBeaconViewHolder
            val ibeacon =  deviceList[position] as IBeacon
            iBeaconView.uuid.text = ibeacon.getUUID()
            iBeaconView.major.text = ibeacon.getMajor().toString()
            iBeaconView.minor.text = ibeacon.getMinor().toString()
            iBeaconView.address.text = ibeacon.getAddress()
            iBeaconView.rssi.text = ibeacon.getRssi().toString()
        }
        VIEW_TYPE_BLE -> {
            val bleView = holder as BLEViewHolder
            val ble =  deviceList[position] as BLEDevice
            bleView.address.text = ble.getAddress()
            bleView.rssi.text = ble.getRssi().toString()
        }
    }

}

Logic improvement

BLEDevice and IBeacon model

In order to store data and display on UI, I need to update these 2 classes:

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
open class BLEDevice(scanResult: ScanResult) {

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

    /**
     * Device mac address
     */
    private var address: String = ""

    /**
     * Device friendly name
     */
    private var name: String = ""


    init {
        if (scanResult.device.name != null) {
            name = scanResult.device.name
        }
        address = scanResult.device.address
        rssi = scanResult.rssi
    }

    fun getAddress(): String {
        return address
    }

    fun getRssi(): Int {
        return rssi
    }
}

IBeacon:

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class IBeacon(scanResult: ScanResult, packetData: ByteArray) : BLEDevice(scanResult) {

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

    /**
     * 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

    init {
        rawByteData = packetData

    }

    /**
     * Parse iBeacon UUID from packet
     */
    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 = ConversionUtils.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
    }

    /**
     * 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
    }

    override fun toString(): String {
        return "Major= " + major.toString() + " Minor= " + minor.toString() + " rssi=" + getRssi()
    }
}

MainAcitivy

Declare new properties in MainActivity

1
2
3
private lateinit var scanService: ScanService
private lateinit var adapter: DeviceListAdapter
private lateinit var deviceList: ArrayList<Any>

Then when activity is created, initialize those properties. Remember to check for permissions before initializing scan service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.scanBtn.setOnClickListener { startScan() }
    binding.exitBtn.setOnClickListener { exitApp() }
    val recycleView: RecyclerView = findViewById(R.id.deviceList)
    deviceList = ArrayList()
    this.adapter = DeviceListAdapter(this.deviceList)
    recycleView.adapter = this.adapter

    // check for permission to scan BLE
    if (isPermissionGranted(this)) {
        Log.d(TAG, "@onCreate init scan service")
        scanService = ScanService(this, this.deviceList, this.adapter)
    }
}

For scanning process, first I will change scan mode to Low latency. This mode will use more power and is preferable when app is in Foreground.

In ScanService constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
constructor(context: Context, deviceList: ArrayList<Any>, adapter: DeviceListAdapter) {
        this.deviceList = deviceList

        builder = ScanSettings.Builder()
        builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
        Log.d(TAG, "set scan mode to low latency")

        this.adapter = adapter
        bluetoothManager = context.getSystemService(BluetoothManager::class.java)
        bluetoothAdapter = bluetoothManager.adapter
        if (bluetoothAdapter == null) {
            // Device doesn't support Bluetooth
            throw Exception("Device doesn't support Bluetooth")
        }
        if (isBluetoothEnabled()) {
            bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
        }
    }

Create a new initScanner, this method is called from MainAcitivity to initialize BLE scanner:

1
2
3
4
5
fun initScanner() {
    if (!this::bluetoothLeScanner.isInitialized) {
        bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
    }
}

Update logic when start scanning, no need to stop scanning with handler since I want the app to actively scan for device:

 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
/**
 * Start BLE scan using bluetoothLeScanner
 * if app is not scanning, start scan by calling startScan and passing a callback method
 * else do nothing (return)
 * @return {none}
 */
fun startBLEScan() {
    if (isScanning)
        return
    Log.d(TAG, "@startBLEScan start beacon scan")
    isScanning = true
    try {
        bluetoothLeScanner.startScan(null, builder.build(), leScanCallback)
    } catch (e: SecurityException) {
        Log.e(TAG, "@startScan SecurityException: " + e.message)
    }
}

fun stopBLEScan() {
    if (!isScanning)
        return
    Log.d(TAG, "@startBLEScan start beacon scan")
    isScanning = false
    bluetoothLeScanner.stopScan(leScanCallback)
}

Whenever the app receives a BLE packet, I want to display it on UI. But if the packet came from device that is already in the list, I want to update the data only. In this case, I will use add logic in checkDeviceExists by checking device address:

 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
51
52
/**
 * callback method when app detects BLE device
 * parse packet to determine if device is iBeacon
 * if device is iBeacon and device is not added in device list, then add to list
 * if device exists in list, update latest value
 */
private val leScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        if (result != null) {
            val scanRecord = result.scanRecord
            Log.e(TAG, "@result: " + result.device.address)
            super.onScanResult(callbackType, result)
            try {
                if (scanRecord != null) {
                    if (isIBeacon(scanRecord.bytes)) {
                        val iBeacon = IBeacon(result, scanRecord.bytes)
                        val idx = checkDeviceExists(result)
                        if (idx == -1) {
                            deviceList.add(iBeacon)
                        } else {
                            // update
                            deviceList[idx] = iBeacon
                        }
                    } else {
                        val ble = BLEDevice(result)
                        val idx = checkDeviceExists(result)
                        if (idx == -1) {
                            deviceList.add(ble)
                        } else {
                            // update
                            deviceList[idx] = ble
                        }
                    }
                    adapter.notifyDataSetChanged()
                }
            } catch (e: SecurityException) {
                Log.e(TAG, "@startScan SecurityException: " + e.message)
            }
        }
        return
    }
}

/**
 * check if our device list already has a scan result whose MAC address is identical to the new incoming ScanResult
 * @param result BLE scan result
 * @return -1 if doesn't exist
 */
fun checkDeviceExists(result: ScanResult): Int {
    val indexQuery = deviceList.indexOfFirst { (it as BLEDevice) .getAddress() == result.device.address }
    return indexQuery
}

Final result

Now build the project, load your app, and put an iBeacon nearby, you should see something like this:

210235.jpg

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

References:

StackOverflow - How to inflate different layout in RecyclerView based on its position in onCreateViewHolder method

StackOverflow - How to create RecyclerView with multiple view types