Skip to Main Content
Engineer working with Android Studio in office

Mastering Permissions for Bluetooth Low Energy Android

Navigating Android’s ever-evolving permissions landscape can be a daunting task for developers building Bluetooth Low Energy (BLE) applications. Over the years, Android OS updates have introduced new permissions, modified existing ones, and changed how developers must approach permissions for BLE-related functionality. 

Keeping up with these changes can feel like aiming at a moving target, especially when each version of Android brings its own set of requirements. Whether it’s managing runtime permissions, addressing user denials, or making complex requests more user-friendly, understanding and adapting to these challenges is essential for building reliable and user-focused BLE apps.

Why Should You Trust Us?

At Punch Through, we understand these challenges intimately. With over a decade of experience tackling BLE-specific issues, we’ve encountered and solved countless problems that developers like you face daily. As the creators of the LightBlue app—a widely trusted tool in the BLE development community—we’ve worked extensively with BLE permissions and learned firsthand what works, what doesn’t, and how to make the process more efficient.

What’s In This Article?

This article is designed to be your practical, code-focused resource for managing BLE permissions on Android. Our goal is to cut through the complexity and provide clear, actionable solutions to help you streamline your development process. Whether you’re struggling with permissions setup, runtime management, or user education, this guide will equip you with the knowledge to handle it all.Before diving in, note that this resource assumes you already have your BLE project set up. If you’re still in the early stages and need help getting started, we recommend checking out our Ultimate Guide to Android BLE, where we cover project setup in detail. Once you’re ready, let’s dive into the intricacies of permissions handling and take your BLE app to the next level.

Code Companion & Examples

All the code snippets in this guide aim to showcase how a given BLE operation should be performed. The actual, full implementation in the context of an example app is available on our open-source GitHub repo.

Note: As of April 17, 2024, we’ve updated this entire repo to support a targetSdk of Android 14, along with new Nearby Devices permission handling for Android 12+ and the new BluetoothGattCallback methods for Android 13+.

Email Sign-up. Subscribe to stay informed.

Permissions Handling

Why Are Permissions So Important?

Permissions are foundational to building BLE apps, directly influencing your app’s ability to access BLE hardware, comply with Android’s safety requirements, and deliver a positive user experience. From the very beginning of app development, understanding your permissions requirements and implementing appropriate handling strategies is essential to ensuring your app can scan for, connect to, and exchange data with BLE devices

Beyond functionality, permissions are critical for building privacy and trust into your app. BLE functionality often requires access to sensitive user data, such as location, which can raise concerns for users. Implementing permissions thoughtfully during development helps you create an app that is transparent, respects user privacy, and builds trust. Android’s permissions framework ensures that users stay in control of their data, and it’s your responsibility as a developer to design an app that aligns with these expectations.

With each new Android version, permissions policies evolve to address emerging threats and ensure apps handle sensitive data appropriately. This ever-changing landscape makes staying up-to-date on Android permissions requirements essential for BLE developers. Each Android update introduces new rules or adjustments that can impact how your app functions. Falling behind on these changes can lead to errors, crashes, or even the inability for your app to access BLE hardware at all. 

By understanding the dependencies between BLE functionality and permissions—and adapting your app as policies evolve—you ensure your app remains secure, compliant, and functional, delivering the best possible experience to your users.

Declaring Required Permissions in AndroidManifest.xml

Now that we’ve established why permissions are essential to BLE app functionality and user trust, let’s start with the first step in the permissions implementation process: declaring the required permissions in your AndroidManifest.xml file.

This file serves as the blueprint for your app’s permissions, informing Android about the features your app intends to use. Without the proper declarations here, your app won’t even get off the ground when it comes to accessing BLE hardware.

Whether you’re starting a new project or revisiting an existing one, ensuring that all necessary permissions are included is crucial for avoiding runtime issues and ensuring compliance with Android’s evolving policies. Let’s walk through how to set this up.

Begin by opening your project’s  AndroidManifest.xml . You can find this file under app → manifests → AndroidManifest.xml in your project’s tool window. Within the <manifest> tag, you’ll need to include permissions that allow your app to perform core BLE tasks, such as scanning for and connecting to devices. Here’s what you’ll need to add:

<!-- Request legacy Bluetooth permissions on versions older than API 31 (Android 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.ACCESS_COARSE_LOCATION"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="30" />

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation"
    tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

Here’s what each of these permissions do:

  • BLUETOOTH: Legacy permission that allows the app to connect to Bluetooth devices.
  • BLUETOOTH_ADMIN: Legacy permission that allows the app to scan for and bond with Bluetooth devices.
  • ACCESS_FINE_LOCATION: Between Android 6 and Android 11 (inclusive on both ends), location permission is required for the app to get BLE scan results. The main motivation behind having to require the users to grant this permission explicitly is to protect users’ privacy.  A BLE scan can often unintentionally reveal the user’s location to unscrupulous app developers who scan for specific BLE beacons, or some BLE devices may advertise location-specific information. Before Android 10, ACCESS_COARSE_LOCATION can be used to gain access to BLE scan results, but we recommend using ACCESS_FINE_LOCATION instead since it works for all versions of Android.
  • ACCESS_COARSE_LOCATION: Apps targeting Android 12 (API 31) and above must also request this permission in addition to ACCESS_FINE_LOCATION.
  • BLUETOOTH_SCAN: For apps targeting Android 12 and above, developers can finally request explicit permission to perform only Bluetooth scans without having to obtain location access for devices running Android 12 and above.
  • BLUETOOTH_CONNECT: For apps targeting Android 12 and above, developers can request this permission to connect to Bluetooth peripherals that are currently bonded to an Android device running Android 12 and above.

Out of all these permissions, ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION, BLUETOOTH_SCAN, and BLUETOOTH_CONNECT are considered — in Android terms — dangerous or runtime permissions. What this means is that while some permissions are automatically granted to the app during installation time, the user needs to explicitly grant these permissions to the app from the app UI. It’s of utmost importance to stress to the user that your app won’t use their location for anything aside from ensuring BLE scanning works if that’s the case.

If BLE hardware is a requirement for your app, you can optionally declare that your app uses BLE features on Android devices. By doing so, users on devices without BLE capabilities won’t see your app on the Google Play Store. If this behavior sounds good, add the following snippet below the <uses-permission> tags.

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

Requesting Runtime Permissions From the User

On Android, runtime permissions are used to enhance user control and privacy. Unlike install-time permissions, which are granted automatically when the app is installed, runtime permissions require users to explicitly grant access to sensitive features or data during app usage. This ensures that users are fully aware of and in control of the data or functionalities the app accesses at specific moments.

For Bluetooth Low Energy (BLE) apps, runtime permissions are essential because BLE functionality often involves accessing sensitive user data, such as location or nearby devices, which Android classifies as potentially privacy-invading. Without these permissions, your app cannot initiate key BLE operations, like scanning for or connecting to devices.

One of the things we recommend you have in your ActivityViewModel, or at least in a Context extension function, is a way to know whether the user has granted the required runtime permissions. The following Context extension functions work together to do just this:

/**
 * Determine whether the current [Context] has been granted the relevant [Manifest.permission].
 */
fun Context.hasPermission(permissionType: String): Boolean {
    return ContextCompat.checkSelfPermission(this, permissionType) ==
        PackageManager.PERMISSION_GRANTED
}

/**
 * Determine whether the current [Context] has been granted the relevant permissions to perform
 * Bluetooth operations depending on the mobile device's Android version.
 */
fun Context.hasRequiredBluetoothPermissions(): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        hasPermission(Manifest.permission.BLUETOOTH_SCAN) &&
            hasPermission(Manifest.permission.BLUETOOTH_CONNECT)
    } else {
        hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
    }
}

The two functions above are extension functions on the Context object, meaning each Context instance with access to these functions can call them and use them as if these functions were part of the original class declaration. Depending on how you structure your source code, these extension functions may reside in another Kotlin source file so that they can be accessed by other Activities as well.

Now add a top-level constant declaration outside your Activity‘s class declaration — the actual value of the constant can be any positive integer you want:

private const val PERMISSION_REQUEST_CODE = 1

Since the runtime permissions are only needed from the moment when the user wants to perform a BLE scan, it makes sense that we should only prompt the user to grant this permission when they want to initiate a BLE scan. The following snippets assume you have a Button that’ll start scanning for BLE devices when tapped.

In that Button’s OnClickListener, we want to call a function startBleScan(). If that’s the only thing the Button will do, your code should look something like this:

scanButton.setOnClickListener { startBleScan() }

startBleScan() will essentially check to see if the required runtime permissions have been granted before allowing the user to proceed with performing a BLE scan.

private fun startBleScan() {
    if (!hasRequiredBluetoothPermissions()) {
        requestRelevantRuntimePermissions()
    } else { /* TODO: Actually perform scan */ }
}

private fun Activity.requestRelevantRuntimePermissions() {
    if (hasRequiredBluetoothPermissions()) { return }
    when {
        Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> {
            requestLocationPermission()
        }
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            requestBluetoothPermissions()
        }
    }
}

private fun requestLocationPermission() = runOnUiThread {
    AlertDialog.Builder(this)
        .setTitle("Location permission required")
        .setMessage(
            "Starting from Android M (6.0), the system requires apps to be granted " +
            "location access in order to scan for BLE devices."
        )
        .setCancelable(false)
        .setPositiveButton(android.R.string.ok) { _, _ ->
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                PERMISSION_REQUEST_CODE
            )
        }
        .show()
}

@RequiresApi(Build.VERSION_CODES.S)
private fun requestBluetoothPermissions() = runOnUiThread {
    AlertDialog.Builder(this)
        .setTitle("Bluetooth permission required")
        .setMessage(
            "Starting from Android 12, the system requires apps to be granted " +
                "Bluetooth access in order to scan for and connect to BLE devices."
        )
        .setCancelable(false)
        .setPositiveButton(android.R.string.ok) { _, _ ->
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.BLUETOOTH_CONNECT
                ),
                PERMISSION_REQUEST_CODE
            )
        }
        .show()
    }
}

startBleScan() will call requestRelevantRuntimePermissions() if we don’t yet have the relevant runtime permissions — this API-dependent check is encapsulated within hasRequiredBluetoothPermissions(), an extension function that we shared earlier.

If at least one runtime permission is missing, an alert will then be shown to inform the user of the need to grant the missing runtime permission(s) and giving them the sole option of “OK,” which will trigger a system dialog prompting the user to grant the relevant permission(s) to the app.

We’ll now need to do the same thing as before and react to the user’s actions, this time by overriding the onRequestPermissionsResult() function on ActivityCompat to handle the case when the requestCode is equal to PERMISSION_REQUEST_CODE:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode != PERMISSION_REQUEST_CODE) return

    val containsPermanentDenial = permissions.zip(grantResults.toTypedArray()).any {
        it.second == PackageManager.PERMISSION_DENIED &&
            !ActivityCompat.shouldShowRequestPermissionRationale(this, it.first)
    }
    val containsDenial = grantResults.any { it == PackageManager.PERMISSION_DENIED }
    val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
    when {
        containsPermanentDenial -> {
            // TODO: Handle permanent denial (e.g., show AlertDialog with justification)
            // Note: The user will need to navigate to App Settings and manually grant
            // permissions that were permanently denied
        }
        containsDenial -> {
            requestRelevantRuntimePermissions()
        }
        allGranted && hasRequiredBluetoothPermissions() -> {
            startBleScan()
        }
        else -> {
            // Unexpected scenario encountered when handling permissions
            recreate()
        }
    }
}

If the user agrees to grant the app with the runtime permissions that we’ve requested, we’re good to go and can start scanning for BLE devices; otherwise, we’ll keep asking them to grant those permissions if they have yet to permanently deny the request by checking the “Don’t ask again” box.

Note on Android 11 and newer: A repeated denial is implicitly treated as a permanent denial by Android, and the app can no longer prompt the user to grant those permissions. In this case, developers should have proper UX explaining why the permissions are needed and how to remedy this situation by directing the users to App Settings so they can manually grant those denied permissions. A common way of doing so is by launching an Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) and by setting the Intent‘s data to a specific URI which describes the app’s package name, e.g., package:com.punchthrough.lightblueandroid.

Again, a well-designed app should have a more graceful way of handling the various rejection scenarios, but we’ll leave that as an exercise for the reader. 

Onwards!

At this point, we’d like you to once again build and run the app on a physical Android device. If your Android device is running Android 6–11 (inclusive on both ends of the range), you should see a location access prompt appear as you tap “Start Scan”; if your device is running Android 12 or newer, you should see a nearby Bluetooth devices access prompt appear instead. Verify that the deny option will cause the permission request prompt to reappear, after which you should grant the requested permissions to avoid getting into the permanent denial execution branch.

For the sake of brevity, we’ll assume these runtime permissions have been granted for all the following sections.

Making Sure That Bluetooth is Enabled

If Bluetooth being enabled is crucial to your app’s core functionality, you’ll want to make sure that your users keep Bluetooth enabled while they’re using your app. Fortunately, Android provides an Intent action that can prompt a user to turn on Bluetooth on their device. The only caveat is that if your app targets Android 12 or higher, the new BLUETOOTH_CONNECT permission must be granted before you can use this Intent action.

In our example below, we want to check in the Activity’s onResume() if Bluetooth is enabled; if it’s not, we display an alert:

private val bluetoothAdapter: BluetoothAdapter by lazy {
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothManager.adapter
}

private val bluetoothEnablingResult = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        // Bluetooth is enabled, good to go
    } else {
        // User dismissed or denied Bluetooth prompt
        promptEnableBluetooth()
    }
}

override fun onResume() {
    super.onResume()
    if (!bluetoothAdapter.isEnabled) {
        promptEnableBluetooth()
    }
}

/**
 * Prompts the user to enable Bluetooth via a system dialog.
 *
 * For Android 12+, [Manifest.permission.BLUETOOTH_CONNECT] is required to use
 * the [BluetoothAdapter.ACTION_REQUEST_ENABLE] intent.
 */
private fun promptEnableBluetooth() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
        !hasPermission(Manifest.permission.BLUETOOTH_CONNECT)
    ) {
        // Insufficient permission to prompt for Bluetooth enabling
        return
    }
    if (!bluetoothAdapter.isEnabled) {
        Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE).apply {
            bluetoothEnablingResult.launch(this)
        }
    }
}

When your first Activity is about to show with these additions, we check if the BluetoothAdapter is enabled. If it’s not, we display a system alert utilizing BluetoothAdapter. ACTION_REQUEST_ENABLE to request that the user enable Bluetooth on their Android device. Remember that you need the BLUETOOTH_CONNECT permission before you can launch this Intent on Android 12 and above!

At this point, take the time to build and run the app on a physical Android device. We’re using an Android physical device because simulators don’t yet come with Bluetooth support at the time of writing. You’ll notice that if you disable Bluetooth and get to your Activity, a system dialog prompts you to enable Bluetooth. If you dismiss the dialog, the app attempts to keep displaying the same alert over and over again until the user accepts our recommendation to turn on Bluetooth.

In a production app, there’ll likely be some kind of UX around this to educate the users on why they should enable Bluetooth. Instead of the very crude example of calling promptEnableBluetooth(), you can either display a new informational alert that’ll later call into your function to display the Bluetooth-enabling system alert again or kick-start some other fancy user education flow.

The Android Studio “MissingPermission” Warning

The code samples in the following sections assume that they’ll only ever be called by the app code when the user has already granted all the relevant runtime permissions.

For the purposes of running our sample code, if you’re encapsulating all the BLE scanning and connecting logic in one or two main classes, you may want to add a @SuppressLint for the specific “MissingPermission” warning that Android Studio may emit for classes that utilize the Android Bluetooth APIs without an explicit runtime permission check surrounding these usages.

@SuppressLint("MissingPermission") // App's role to ensure permissions are available
class BluetoothManager { ... }

Final Thoughts…

To recap, permissions are the backbone of BLE app functionality. They ensure your app can access essential hardware, comply with Android’s security framework, and create a seamless, privacy-conscious user experience. By understanding why permissions matter, how they interact with BLE functionality, and implementing best practices for managing them, you’re setting your app up for success.

But handling permissions is just the beginning of building a robust BLE app. Once you’ve nailed down your permissions setup, the next steps involve performing a scan, discovering services, and diving into data transfer through read/write operations. If you’re ready to move on, we recommend exploring our Ultimate Guide to Android BLE for a detailed walkthrough of these advanced steps.If you’re looking to go deeper into BLE development, check out our resource page for more insights into advanced BLE functionalities. And if you’re still facing challenges—whether with permissions or any other part of your BLE app development process—Punch Through is here to help. With our extensive BLE expertise and hands-on experience, we’re ready to tackle even the toughest development roadblocks with you.

Punch Through's Mobile App Development for Android

Related BLE Articles

For more expert connectivity development resources, check out our other great resources:

Bluetooth Connectivity Architecture Guide

Explore the foundational topics you need to understand and effectively plan and architect a BLE-connected device. Plus, get an inside look at how we at Punch Through tackle the discovery, planning, and architecture stages.

Ultimate Guide for Managing Your BLE Connection

Learn all the basics of managing your BLE connection, peripheral advertising, and power consumption vs latency considerations for connected devices.