How to Create a Bluetooth LE Scanner for Android
Tags: android, kotlin, tutorial, mobile
Published: 2023-08-31 17:37:15

Creating a Bluetooth LE scanner for Android is not too complicated. What you need is some understanding of the Android architecture and of Java/Kotlin.

The most difficult part of building such an app would be…managing the permissions, or even finding what are the required permissions (at least for me).

## What You Need to Follow This Tutorial

-   Android Studio installed on your computer
-   A BLE enabled smartphone for testing running Android 12 (or later)
-   A basic understanding of Kotlin (or Java, at least)
-   A (very) basic understanding of how to create a UI in Android

## Creating the Project

I will assume that you already know how to create a project in Android Studio, so create a new project, choose _Empty Activity_, and name it whatever you like, I named mine _SimpleBleScanner_. Also, select _API 31_ as the _Minimum SDK_.  
I know that requiring Android 12 could be a bit too tight of a requirement, but this choice was made to be able to experiment with the latest features.  
Anyway, if you have the necessity to use an earlier version of the SDK, I think you won’t have to change much of the code.

## Managing the Permissions

First, we are going to declare in the Manifest (`AndroidManifest.xml`) all the permissions we will use in the application. In particular, for Bluetooth LE scanning, the minimum required permissions are the following:
```xml
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
```
As weird as it might sound, access to coarse and fine location is required in order to use BLE.

Next, inside the package containing the `MainActivity`, I chose to create another package named `permissions`, in which to put all the permission utilities.

Inside this newly created package, create two Kotlin objects named `PermissionsUtilities` and `BleScanRequiredPermissions`. These two objects will help us in managing the permissions without writing too much code inside the  `MainActivity`.

`BleScanRequiredPermissions` will contain only the array of the permissions we are going to require for our app.
```kotlin
val permissions = arrayOf(
    Manifest.permission.BLUETOOTH_SCAN,
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.BLUETOOTH_ADMIN,
)
```
`PermissionsUtilities`, on the other hand, is a more complex object, containing all the utility functions to manage permissions in our application.  
Anyway, the focus of the article is not on permissions handling, so I will go fast on this part. Feel free to check the `permissions` package by yourself if interested.

The main functions of this module are:
```kotlin
/**
* Checks for a set of permissions. If not granted, the user is asked to grant them.
*
* @param activity The activity that is requesting the permissions
* @param permissions The permissions to be checked
* @param requestCode The request code to be used when requesting the permissions
*/
fun checkPermissions(activity: Activity, permissions: Array<out String>, requestCode: Int)

/**
* Checks whether a set of permissions is granted or not
*
* @param context The context to be used for checking the permissions
* @param permissions The permissions to be checked
*
* @return true if all permissions are granted, false otherwise
*/
fun checkPermissionsGranted(context: Context, permissions: Array<out String>): Boolean

/**
* Checks the result of a permission request, and dispatches the appropriate action
*
* @param requestCode The request code of the permission request
* @param grantResults The results of the permission request
* @param onGrantedMap maps the request code to the action to be performed if the permissions are granted
* @param onDeniedMap maps the request code to the action to be performed if the permissions are not granted
*/
fun dispatchOnRequestPermissionsResult(
    requestCode: Int, grantResults: IntArray,
    onGrantedMap: Map<Int, () -> Unit>,
    onDeniedMap: Map<Int, () -> Unit>
)

/**
* Checks whether a permission is granted in the context
*
* @param context The context to be used for checking the permission
* @param permission The permission to be checked
*
* @return true if the permission is granted, false otherwise
*/
private fun checkPermissionGranted(context: Context, permission: String): Boolean

/**
* Checks the results of a permission request
*
* @param grantResults The results of the permission request
*
* @return true if all permissions were granted, false otherwise
*/
private fun checkGrantResults(grantResults: IntArray): Boolean
```

Otherwise, you could check this article about a permission management library I made, and try to use it instead… [Android Permissions Made Easy](https://medium.com/geekculture/android-permissions-made-easy-d5e3e81ba2ac).

## Scanning for devices

To manage the scanning, we will create a new package named `blescanner`, containing two sub-packages named `model` and `adapter`.  
The `model` the classes modeling the `BleDevice` and the  `BleScanCallback`, while the `adapter` will contain the  `BleDeviceAdapter`, used as adapter for the `RecyclerView` showing the results of the scan.
```kotlin
//BleDevice.kt
data class BleDevice(val name: String) {
    companion object {
        fun createBleDevicesList(): MutableList<BleDevice> {
            return mutableListOf()
        }
    }
}
```

```kotlin
// BleDeviceAdapter.kt
class BleDeviceAdapter(private val devices: List<BleDevice>) : RecyclerView.Adapter<BleDeviceAdapter.ViewHolder>() {
    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val deviceNameTextView: TextView = itemView.findViewById<TextView>(R.id.device_name)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val context = parent.context
        val inflater = LayoutInflater.from(context)
        val deviceView = inflater.inflate(R.layout.device_row_layout, parent, false)
        return ViewHolder(deviceView)
    }

    override fun onBindViewHolder(holder: BleDeviceAdapter.ViewHolder, position: Int) {
        val device = devices[position]
        val textView = holder.deviceNameTextView
        textView.text = device.name
    }

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

Other than these three classes, we will create a  `BleScanManager`, that will manage the scanning.

The `BleScanManager` class contains various members to manage the scanning state and what to do before/after the scanning.

```kotlin

class BleScanManager(
    btManager: BluetoothManager,
    private val scanPeriod: Long = DEFAULT_SCAN_PERIOD,
    private val scanCallback: BleScanCallback = BleScanCallback()
) {
    private val btAdapter = btManager.adapter
    private val bleScanner = btAdapter.bluetoothLeScanner

    var beforeScanActions: MutableList<() -> Unit> = mutableListOf()
    var afterScanActions: MutableList<() -> Unit> = mutableListOf()

    /** True when the manager is performing the scan */
    private var scanning = false

    private val handler = Handler(Looper.getMainLooper())

    /**
     * Scans for Bluetooth LE devices and stops the scan after [scanPeriod] seconds.
     * Does not checks the required permissions are granted, check must be done beforehand.
     */
    @SuppressLint("MissingPermission")
    fun scanBleDevices() {
        fun stopScan() {
            scanning = false
            bleScanner.stopScan(scanCallback)

            // execute all the functions to execute after scanning
            executeAfterScanActions()
        }

        // scans for bluetooth LE devices
        if (scanning) {
            stopScan()
        } else {
            // stops scanning after scanPeriod millis
            handler.postDelayed({ stopScan() }, scanPeriod)
            // execute all the functions to execute before scanning
            executeBeforeScanActions()

            // starts scanning
            scanning = true
            bleScanner.startScan(scanCallback)
        }
    }
}
```

The constructor will take as input a `BluetoothManager`, a scan period (set to a default value if omitted), and a list of actions to do before and after the scanning. The list of actions is nothing but a list of functions that will be invoked before/after the scanning start.

The `Handler` is used to schedule the stopping of the scan after the scan period time.

The companion object of the `BleScanManager` will contain the default value of the scan period, and a function to execute the list of actions.

```kotlin
companion object {
    const val DEFAULT_SCAN_PERIOD: Long = 10000

    private fun executeListOfFunctions(toExecute: List<() -> Unit>) {
        toExecute.forEach {
            it()
        }
    }
```

The last lines to complete the `BleScanManager` are the following
```kotlin
private fun executeBeforeScanActions() {
    executeListOfFunctions(beforeScanActions)
}

private fun executeAfterScanActions() {
    executeListOfFunctions(afterScanActions)
}
```

## The Main Activity

The main activity will contain the code that “connects” all the modules we created so far:

-   the module managing the permissions
-   the BLE scan manager
-   the adapter for the `RecyclerView`

The code is pretty straightforward, the only two methods are the `onCreate` and the `onRequestPermissionResult`.

```kotlin
class MainActivity : AppCompatActivity() {
    private lateinit var btnStartScan: Button
    private lateinit var btManager: BluetoothManager
    private lateinit var bleScanManager: BleScanManager
    private lateinit var foundDevices: MutableList<BleDevice>

    @SuppressLint("NotifyDataSetChanged", "MissingPermission")
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // RecyclerView handling
        val rvFoundDevices = findViewById<View>(R.id.rv_found_devices) as RecyclerView
        foundDevices = BleDevice.createBleDevicesList()
        val adapter = BleDeviceAdapter(foundDevices)
        rvFoundDevices.adapter = adapter
        rvFoundDevices.layoutManager = LinearLayoutManager(this)

        // BleManager creation
        btManager = getSystemService(BluetoothManager::class.java)
        bleScanManager = BleScanManager(btManager, 5000, scanCallback = BleScanCallback({
            val name = it?.device?.address
            if (name.isNullOrBlank()) return@BleScanCallback

            val device = BleDevice(name)
            if (!foundDevices.contains(device)) {
                foundDevices.add(device)
                adapter.notifyItemInserted(foundDevices.size - 1)
            }
        }))

        // Adding the actions the manager must do before and after scanning
        bleScanManager.beforeScanActions.add { btnStartScan.isEnabled = false }
        bleScanManager.beforeScanActions.add {
            foundDevices.clear()
            adapter.notifyDataSetChanged()
        }
        bleScanManager.afterScanActions.add { btnStartScan.isEnabled = true }

        // Adding the onclick listener to the start scan button
        btnStartScan = findViewById(R.id.btn_start_scan)
        btnStartScan.setOnClickListener {
            // Checks if the required permissions are granted and starts the scan if so, otherwise it requests them
            when (PermissionsUtilities.checkPermissionsGranted(
                this,
                BleScanRequiredPermissions.permissions
            )) {
                true -> bleScanManager.scanBleDevices()
                false -> PermissionsUtilities.checkPermissions(
                    this, BleScanRequiredPermissions.permissions, BLE_PERMISSION_REQUEST_CODE
                )
            }
        }
    }

    @RequiresApi(Build.VERSION_CODES.S)
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<out String>, grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        dispatchOnRequestPermissionsResult(
            requestCode,
            grantResults,
            onGrantedMap = mapOf(BLE_PERMISSION_REQUEST_CODE to { bleScanManager.scanBleDevices() }),
            onDeniedMap = mapOf(BLE_PERMISSION_REQUEST_CODE to { Toast.makeText(this,
                    "Some permissions were not granted, please grant them and try again",
                    Toast.LENGTH_LONG).show() })
        )
    }

    companion object { private const val BLE_PERMISSION_REQUEST_CODE = 1 }
}
```

The latter is used to manage what to do “after” requesting the permissions to the user, while the former sets the ground for the application by setting the `RecyclerView` adapter, creating the `BleScanManager`, and setting the `onClickListener` for the button to start the scan.

Right after the `BleScanManager` is created, we add to it `before`/`afterScanActions` the code to enable/disable the scan button — because we want to disable the scan button while we’re performing it.

The `onClickListener` to start the scan checks first that the required permissions are granted, and, if so, it proceeds to start the scan, otherwise it starts permissions request flow.

## The GUI

The GUI will be very (very) simple, just a `Button` to start the scanning, and a `RecyclerView` to show the scanning results.
```xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_start_scan"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_start_scan_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.844" />

    <TextView
        android:id="@+id/lbl_scan_results"
        android:layout_width="163dp"
        android:layout_height="19dp"
        android:layout_marginTop="28dp"
        android:clickable="false"
        android:gravity="center"
        android:text="@string/tw_scan_results_title"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_found_devices"
        android:layout_width="375dp"
        android:layout_height="516dp"
        app:layout_constraintBottom_toTopOf="@+id/btn_start_scan"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.444"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_scan_results"
        app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
```

Other than the `activity_main.xml` we will need another layout file to define the layout of a `RecyclerView` row.

```xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="10dp"
    android:paddingBottom="10dp"
    >
    <TextView
        android:id="@+id/device_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        />
</LinearLayout>
```

## Conclusions

This is all to create a simple Bluetooth LE scanner app in Android.  
The results will look something like this


![final result](https://miro.medium.com/v2/resize:fit:875/1*O_3WualBNIkvummQJpHhXw.png)

The application's UI while performing a scan

As you can see, the app is very simple on the UI side, this because I wanted to focus more on the functionality. If you want, feel free to fork the  [repo](https://github.com/lorenzofelletti/SimpleBleScanner)  and improve the application’s UI, or extend its functionalities.

This is all for this article, I hope you enjoyed reading it and that you found it useful. Let me know with a comment!

---
Cover image by <a href="https://unsplash.com/it/@zarakvg?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Zarak Khan</a> on <a href="https://unsplash.com/it/foto/dV5fxEZaS1A?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>