Ble AdvertiserMaster Android 12+ BLE permissions! This guide details `BLUETOOTH_SCAN`, `CONNECT`, `ADVERTISE` in Kotlin, covering manifest, runtime, and critical best prac...
Did your rock-solid BLE app suddenly stop discovering devices, connecting, or advertising services on Android 12 (API 31) or later? You're not alone. Google's ongoing commitment to user privacy and granular control introduced a significant overhaul to how Bluetooth permissions are handled, moving away from the broad strokes of BLUETOOTH_ADMIN and ACCESS_FINE_LOCATION to a much more precise model. This change is mandatory, not optional, and understanding it is critical to delivering a reliable BLE experience.
This article cuts through the noise, providing a direct, actionable guide for Android developers using Kotlin. We’ll break down the new BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and BLUETOOTH_ADVERTISE permissions, show you exactly how to implement them, and arm you with best practices to avoid common pitfalls.
Before Android 12 (API 31), BLE operations primarily relied on two install-time permissions: BLUETOOTH and BLUETOOTH_ADMIN. While BLUETOOTH_ADMIN granted broad control over Bluetooth functions like scanning and connecting, the critical piece for scanning nearby devices was the runtime permission ACCESS_FINE_LOCATION. This was a point of frequent user friction; many users questioned why a flashlight app needed their precise location, simply because it also wanted to scan for a nearby BLE lightbulb.
With Android 12, Google addressed this by decoupling location from fundamental BLE operations. The new model introduces three distinct runtime permissions, giving users more granular control over what specific Bluetooth capabilities your app can access.
Here's the breakdown of the new permissions introduced in API 31+:
BLUETOOTH_SCAN
BluetoothAdapter.startLeScan() or BluetoothLeScanner.startScan().BLUETOOTH_SCAN does NOT inherently require ACCESS_FINE_LOCATION just to discover devices. If your app uses scan results only to find a device for connection and doesn't derive physical location from these results, you do not need to request ACCESS_FINE_LOCATION alongside BLUETOOTH_SCAN. This is a significant privacy improvement. You should also include android:usesPermissionFlags="neverForLocation" in your manifest for BLUETOOTH_SCAN to explicitly signal this intent to the OS.BLUETOOTH_CONNECT
BluetoothDevice.connectGatt()).BluetoothAdapter.listenUsingRfcommWithServiceRecord(), BluetoothGattServer.accept()).BluetoothAdapter.getProfileProxy()).BLUETOOTH_ADVERTISE
BluetoothLeAdvertiser.startAdvertising()).While BLUETOOTH_SCAN now largely stands alone for discovery, there are specific scenarios where ACCESS_FINE_LOCATION is still required for BLE operations, even on Android 12+:
ACCESS_FINE_LOCATION is still necessary. The system needs to ensure the user is aware their location is being derived.BLUETOOTH_ADMIN and ACCESS_FINE_LOCATION were granted. However, this is a compatibility bridge, and you must update your targetSdkVersion to 31 or higher and request the new permissions directly.ACCESS_FINE_LOCATION. On API 31+, BLUETOOTH_SCAN (especially with neverForLocation) allows an app to bypass this global setting for app-initiated scans if the app doesn't derive location from scan results.Summary Table: Permissions Pre- vs. Post-API 31
| Operation | Android 11 (API 30) & Lower | Android 12 (API 31) & Higher | Notes |
|---|---|---|---|
| Scanning |
BLUETOOTH_ADMIN + ACCESS_FINE_LOCATION (runtime) |
BLUETOOTH_SCAN (runtime) |
ACCESS_FINE_LOCATION no longer required for mere discovery on API 31+ if not inferring location. |
| Connecting | BLUETOOTH_ADMIN |
BLUETOOTH_CONNECT (runtime) |
No runtime permission for connection prior to API 31. |
| Advertising | BLUETOOTH_ADMIN |
BLUETOOTH_ADVERTISE (runtime) |
No runtime permission for advertising prior to API 31. |
| Location Use |
ACCESS_FINE_LOCATION (runtime) |
ACCESS_FINE_LOCATION (runtime) |
Still required if your app uses scan results to derive physical location. |
Implementing the new BLE permissions correctly involves two main steps: declaring them in your AndroidManifest.xml and then requesting them at runtime using modern Activity Result APIs.
AndroidManifest.xml
You need to declare the new Bluetooth permissions in your AndroidManifest.xml. It's crucial to also include maxSdkVersion for older permissions if you still need to support pre-API 31 devices, ensuring the system selects the correct permissions based on the device's OS version.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- REQUIRED for devices running Android 12 (API level 31) or higher -->
<!-- For discovering nearby BLE devices -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<!-- For connecting to already-paired BLE devices and communicating -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"
tools:targetApi="s" />
<!-- For making your device discoverable as a BLE peripheral -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"
tools:targetApi="s" />
<!-- REQUIRED for devices running Android 11 (API level 30) or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!--
REQUIRED if your app still needs to derive physical location from BLE scan results,
OR if targeting API 30 or lower and performing BLE scans.
For API 31+, BLUETOOTH_SCAN with "neverForLocation" flag covers most scan use cases.
-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Declares that your app uses BLE and should only be installed on devices that support it. -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BlePermissionsGuide"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Key Points in Manifest:
android:usesPermissionFlags="neverForLocation": Crucial for BLUETOOTH_SCAN. This flag explicitly tells the system that your app does not intend to use Bluetooth scan results to derive physical location. This can help prevent unnecessary location permission prompts and streamline the user experience.tools:targetApi="s": The tools:targetApi attribute (from xmlns:tools) helps lint and other build tools understand that these permissions are specific to certain API levels, preventing warnings if your minSdkVersion is lower than 31.android:maxSdkVersion="30": This ensures that older permissions like BLUETOOTH and BLUETOOTH_ADMIN are only requested on devices running Android 11 or lower, preventing redundant or incorrect permission requests on newer OS versions.ACCESS_FINE_LOCATION: Keep this if you truly need location for other features or if you are inferring location from BLE scans. Otherwise, consider removing it for maximum privacy.For modern Android development, you should use the registerForActivityResult API (part of the Activity Result APIs) to handle runtime permission requests. This replaces the deprecated onRequestPermissionsResult callback, leading to cleaner and more lifecycle-aware code.
First, define a launcher for requesting multiple permissions in your Activity or Fragment:
import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
private val TAG = "BLEPermissionsGuide"
private var bluetoothAdapter: BluetoothAdapter? = null
// Register ActivityResultLauncher for requesting multiple permissions
private val requestBlePermissions: ActivityResultLauncher<Array<String>> =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val allGranted = permissions.entries.all { it.value }
if (allGranted) {
// All required BLE permissions are granted. Proceed with BLE operations.
Log.d(TAG, "All required BLE permissions granted.")
ensureBluetoothEnabled()
} else {
// At least one BLE permission was denied.
Log.w(TAG, "One or more BLE permissions denied.")
showPermissionDeniedDialog()
}
}
// Register ActivityResultLauncher for enabling Bluetooth
private val enableBluetoothLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
Log.d(TAG, "Bluetooth enabled by user.")
startBleOperationsIfReady()
} else {
Log.w(TAG, "Bluetooth not enabled by user.")
Toast.makeText(this, "Bluetooth is required for this app.", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize BluetoothAdapter
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
findViewById<Button>(R.id.start_scan_button).setOnClickListener {
// Initiate the permission check and request flow
checkAndRequestBlePermissions()
}
}
private fun checkAndRequestBlePermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31) and higher
val permissionsToRequest = mutableListOf<String>()
// Check BLUETOOTH_SCAN
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_SCAN)
}
// Check BLUETOOTH_CONNECT
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT)
}
// Check BLUETOOTH_ADVERTISE (only add if your app needs to advertise)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
// If you need advertising, uncomment the line below:
// permissionsToRequest.add(Manifest.permission.BLUETOOTH_ADVERTISE)
}
// If ACCESS_FINE_LOCATION is truly needed for location inference from BLE scans (not just discovery)
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)
// }
if (permissionsToRequest.isNotEmpty()) {
// Request the missing permissions
requestBlePermissions.launch(permissionsToRequest.toTypedArray())
} else {
// All necessary permissions already granted for API 31+
Log.d(TAG, "All API 31+ BLE permissions already granted.")
ensureBluetoothEnabled()
}
} else { // Android 11 (API 30) and lower
// For older Android versions, we primarily need ACCESS_FINE_LOCATION for scanning.
// BLUETOOTH and BLUETOOTH_ADMIN are install-time permissions.
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// Request ACCESS_FINE_LOCATION using the same ActivityResultLauncher
requestBlePermissions.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION))
} else {
Log.d(TAG, "ACCESS_FINE_LOCATION permission already granted for older API.")
ensureBluetoothEnabled()
}
}
}
private fun ensureBluetoothEnabled() {
bluetoothAdapter?.let {
if (!it.isEnabled) {
Log.d(TAG, "Bluetooth is disabled. Requesting user to enable it.")
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
enableBluetoothLauncher.launch(enableBtIntent)
} else {
Log.d(TAG, "Bluetooth is already enabled.")
startBleOperationsIfReady()
}
} ?: run {
Log.e(TAG, "Bluetooth not supported on this device.")
Toast.makeText(this, "Bluetooth not supported on this device.", Toast.LENGTH_LONG).show()
}
}
private fun startBleOperationsIfReady() {
// Double-check if all conditions (permissions, Bluetooth enabled) are met.
// This method can be called after permissions are granted or Bluetooth is enabled.
if (bluetoothAdapter?.isEnabled == true && hasRequiredBlePermissions()) {
Toast.makeText(this, "Starting BLE scan...", Toast.LENGTH_SHORT).show()
Log.d(TAG, "BLE scan initiated.")
// --- Your BLE scanning or connection logic goes here ---
// Example:
// val scanner = bluetoothAdapter?.bluetoothLeScanner
// scanner?.startScan( /* Scan Filters and Settings */ )
} else {
Log.w(TAG, "Cannot start BLE operations: Permissions or Bluetooth not ready.")
}
}
private fun hasRequiredBlePermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
// Add BLUETOOTH_ADVERTISE if required for your app
// && ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED
// Add ACCESS_FINE_LOCATION if required
// && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
} else {
// For API < 31, ensure ACCESS_FINE_LOCATION (if scanning)
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
private fun showPermissionDeniedDialog() {
AlertDialog.Builder(this)
.setTitle("Permissions Required")
.setMessage("BLE operations require Bluetooth permissions. Please grant them in your device settings.")
.setPositiveButton("Go to Settings") { dialog, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
}
}
Explanation of the Code:
requestBlePermissions Launcher: This ActivityResultLauncher is initialized once. When requestBlePermissions.launch() is called with an array of permissions, it triggers the system permission dialog. The lambda then receives a map of permission names to boolean (granted/denied) results.enableBluetoothLauncher Launcher: Separate launcher to handle the result of ACTION_REQUEST_ENABLE which prompts the user to turn on Bluetooth.checkAndRequestBlePermissions():
Build.VERSION.SDK_INT to conditionally handle permissions based on the Android version.BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and optionally BLUETOOTH_ADVERTISE and ACCESS_FINE_LOCATION.ACCESS_FINE_LOCATION (since BLUETOOTH and BLUETOOTH_ADMIN are install-time).permissionsToRequest and then launches the requestBlePermissions launcher.ensureBluetoothEnabled(): Permissions are just one part. You must also ensure the Bluetooth adapter is actually enabled. This function checks bluetoothAdapter.isEnabled and, if necessary, launches an ACTION_REQUEST_ENABLE intent.startBleOperationsIfReady(): This is your entry point for BLE functionality. It's called only after permissions are granted AND Bluetooth is enabled.showPermissionDeniedDialog(): A critical UX component. If permissions are permanently denied (user checks "Don't ask again"), you cannot re-request them programmatically. This dialog informs the user and provides a direct link to your app's settings page, where they can manually grant permissions.hasRequiredBlePermissions(): A utility function to quickly check the current state of necessary permissions.Even with the correct code, nuanced issues can arise. Here are crucial best practices derived from real-world BLE development.
Request Permissions Contextually, Not All at Once:
BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE) to the user at app launch can be overwhelming and lead to denials. If your app only scans occasionally, asking for BLUETOOTH_CONNECT and BLUETOOTH_ADVERTISE upfront is unnecessary and bad UX.BLUETOOTH_SCAN when the user taps a "Start Scan" button. Request BLUETOOTH_CONNECT when they select a device to connect to. This provides clear rationale and increases the likelihood of permission grants.
// Example: Only request BLUETOOTH_ADVERTISE when user explicitly enables advertising
fun enableAdvertisingClicked() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
requestBlePermissions.launch(arrayOf(Manifest.permission.BLUETOOTH_ADVERTISE))
} else {
startAdvertising() // Proceed with advertising
}
} else {
startAdvertising() // For older APIs, BLUETOOTH_ADMIN handles this
}
}
Understand and Utilize android:usesPermissionFlags="neverForLocation":
android:usesPermissionFlags="neverForLocation" with BLUETOOTH_SCAN can inadvertently prompt the user for location permission, even if your app doesn't need it for location inference. This happens if ACCESS_FINE_LOCATION is also declared in your manifest for other purposes.android:usesPermissionFlags="neverForLocation" when declaring BLUETOOTH_SCAN in your AndroidManifest.xml if your app does not derive physical location from BLE scan results. This tells the OS to handle BLUETOOTH_SCAN independently of ACCESS_FINE_LOCATION. Double-check your app's logic; if you're truly not inferring location, this flag is your friend.Provide Clear Rationale and Handle Persistent Denials Gracefully:
shouldShowRequestPermissionRationale(): Before requesting a permission, check shouldShowRequestPermissionRationale(). If it returns true, it means the user has denied the permission at least once, and you should show a custom UI (like an AlertDialog) explaining why the permission is needed before re-requesting it.shouldShowRequestPermissionRationale() returns false after multiple denials), your app cannot request the permission again. Your only recourse is to guide the user to your app's settings page, where they can manually enable the permission. The showPermissionDeniedDialog() in the example code demonstrates this.Manage Bluetooth State Independently:
BluetoothAdapter.isEnabled() before initiating any BLE operation (scan, connect, advertise). If it's disabled, prompt the user to enable it using an Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE). Your enableBluetoothLauncher example covers this.Backward Compatibility for Older Devices:
Build.VERSION.SDK_INT checks, as shown in the checkAndRequestBlePermissions() example. Ensure your AndroidManifest.xml correctly uses maxSdkVersion for older permissions and includes ACCESS_FINE_LOCATION for scanning on pre-API 31 devices if necessary. The system will then handle the permission model appropriate for the device's OS version.Navigating Android 12+ BLE permissions might seem daunting at first glance, but it's a critical step towards building more secure and privacy-respecting applications. By understanding the distinct roles of BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and BLUETOOTH_ADVERTISE, correctly declaring them in your manifest, and requesting them at runtime with the Activity Result APIs, you empower your users with granular control while ensuring your app functions reliably. Remember to prioritize user experience by requesting permissions contextually, clarifying their necessity, and providing clear paths to resolution if permissions are denied. Now, go forth and refactor your BLE apps with confidence. Review your existing BLE applications today and update their permission handling to embrace the modern Android permission model.