Have you ever deployed a perfectly functional BLE application, only for users on newer Android versions (especially Android 12/API 31+) to report that your app suddenly "lost" its ability to scan or connect, even though you meticulously requested ACCESS_FINE_LOCATION? If so, you've encountered the Android 12+ BLE permission paradigm shift, a necessary evolution that often leaves developers scrambling. This article cuts through the noise, providing a direct, no-fluff guide to navigating the new BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions in Kotlin, ensuring your BLE applications function reliably on modern Android devices.
Core Concepts: The Shift to Granular BLE Permissions
Prior to Android 12 (API level 31), the ACCESS_FINE_LOCATION permission was a ubiquitous, if often misunderstood, requirement for nearly all BLE operations. This was primarily due to the potential for BLE scans to infer a user's location. While this blanket approach simplified things in some ways, it also forced users to grant a highly sensitive location permission even when an app's BLE functionality had no actual need for location data.
Android 12 addressed this by introducing a set of new, more granular Bluetooth permissions, decoupling BLE functionality from location access. These permissions are:
-
BLUETOOTH_SCAN: Required for your app to find nearby Bluetooth devices. This replaces the need forACCESS_FINE_LOCATIONfor most BLE scanning scenarios. -
BLUETOOTH_CONNECT: Required for your app to connect to already paired Bluetooth devices or initiate new connections. -
BLUETOOTH_ADVERTISE: Required for your app to make itself discoverable to other Bluetooth devices and broadcast advertisements.
These new permissions are runtime permissions, meaning you must declare them in your AndroidManifest.xml and then explicitly request them from the user at runtime, just like ACCESS_FINE_LOCATION.
Why the Change?
The primary motivations behind this shift are user privacy and control:
- Granular Control: Users can now grant specific Bluetooth capabilities (scan, connect, advertise) without exposing their precise location.
- Improved Transparency: It's clearer to users exactly what permissions an app needs for Bluetooth operations.
- Reduced Friction: For apps that truly don't need location for BLE, users are less likely to deny permissions.
Permission Mapping and Dependencies
It's critical to understand that these new permissions only apply when your app targets Android 12 (API 31) or higher (targetSdk >= 31). If your targetSdk is 30 or lower, the system still uses ACCESS_FINE_LOCATION for BLE scanning, even on an Android 12+ device. This is a common point of confusion and a significant gotcha.
Here's a simplified mapping of old vs. new permissions and their purpose:
| Operation | Android 11 (API 30) & below (if targetSdk <= 30) |
Android 12 (API 31) & above (if targetSdk >= 31) |
Notes |
|---|---|---|---|
| BLE Scanning | ACCESS_FINE_LOCATION |
BLUETOOTH_SCAN |
ACCESS_FINE_LOCATION might still be needed if your app requires location for other features or if specific BLE scan results require location information (e.g., ScanFilter for device name filtering in some specific OS versions/device manufacturers). Also, turning on location services may still be required on some devices to enable BLE scanning, even with BLUETOOTH_SCAN granted. |
| BLE Connecting | BLUETOOTH |
BLUETOOTH_CONNECT |
BLUETOOTH is a normal permission, automatically granted. BLUETOOTH_CONNECT is runtime. |
| BLE Advertising | BLUETOOTH |
BLUETOOTH_ADVERTISE |
BLUETOOTH_ADVERTISE is runtime. |
| Managing Paired Devices | BLUETOOTH |
BLUETOOTH_CONNECT |
Important Nuance: Even with BLUETOOTH_SCAN granted and targetSdk >= 31, a user might still need to have location services enabled on their device for BLE scanning to function correctly. This is a device-level setting, not an app-level permission, and its necessity can vary by Android OEM implementation. Always prompt the user to enable location services if scanning fails, even if BLUETOOTH_SCAN is granted.
Implementation: The Step-by-Step Walkthrough
Let's dive into integrating these new permissions into your Kotlin BLE application.
Prerequisites
- Android Studio Arctic Fox (or newer)
- Kotlin 1.5.0 (or newer)
-
targetSdkset to31or higher in yourbuild.gradle(module-level). This is critical.
android { compileSdk 33 // Or higher defaultConfig { minSdk 21 targetSdk 33 // This is the key // ... } }
1. Manifest Declarations
You need to declare the new Bluetooth permissions in your AndroidManifest.xml. To maintain backward compatibility with devices running Android 11 (API 30) or lower, and to conditionally request ACCESS_FINE_LOCATION only when needed for older OS versions, you'll typically declare both the old and new permissions.
Crucially, for BLUETOOTH_SCAN, you'll use the android:usesPermissionFlags attribute with neverForLocation. This flag explicitly tells the system that your app does not use the permission for location features, which is key to avoiding location permission requests on Android 12+.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Permissions for Android 11 (API 30) and below -->
<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_FINE_LOCATION" android:maxSdkVersion="30" />
<!-- Permissions for Android 12 (API 31) and above -->
<!-- BLUETOOTH_SCAN: Needed to discover devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="31" />
<!-- BLUETOOTH_CONNECT: Needed to connect to devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:targetApi="31" />
<!-- BLUETOOTH_ADVERTISE: Needed to advertise as a peripheral. -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" tools:targetApi="31" />
<!-- Optionally, if your app truly *needs* location for other features or older Android versions -->
<!-- If your minSdk < 31, ACCESS_FINE_LOCATION is still relevant for BLE scanning on older OS versions. -->
<!-- If your app specifically needs to derive location from BLE scans (e.g., for indoor positioning) -->
<!-- then you might still need ACCESS_FINE_LOCATION even on API 31+ for those specific scenarios, -->
<!-- but for standard BLE proximity scans, neverForLocation covers it. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- If you target API 31+, and *only* need coarse location (e.g., for location-based services that don't need fine accuracy) -->
<!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> -->
<application
<!-- ... your application settings ... -->
</application>
</manifest>
Explanation of Manifest Declarations:
-
android:maxSdkVersion="30": This attribute limits theBLUETOOTH,BLUETOOTH_ADMIN, andACCESS_FINE_LOCATIONpermissions to devices running API level 30 or lower. When your app runs on API 31+, these permissions are ignored by the system, preventing conflicts. -
tools:targetApi="31": This attribute is a lint directive that tells Android Studio that this permission is only relevant for API 31 and above. It helps prevent lint warnings about unused permissions on older Android versions and improves readability. -
android:usesPermissionFlags="neverForLocation": This is the most crucial flag forBLUETOOTH_SCAN. It explicitly tells Android that your app does not intend to use the Bluetooth scan results to derive the user's physical location. If you omit this, even on Android 12+, the system might still requireACCESS_FINE_LOCATIONfor scanning.
2. Runtime Permission Request
Now that the permissions are declared, you need to request them at runtime. You should check the Android version and request the appropriate permissions.
Helper function for permission checks:
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
object BluetoothPermissions {
// Permissions required for BLE scanning on Android 12+
val bluetoothScanPermissions = arrayOf(
Manifest.permission.BLUETOOTH_SCAN
)
// Permissions required for BLE connecting on Android 12+
val bluetoothConnectPermissions = arrayOf(
Manifest.permission.BLUETOOTH_CONNECT
)
// Permissions required for BLE advertising on Android 12+
val bluetoothAdvertisePermissions = arrayOf(
Manifest.permission.BLUETOOTH_ADVERTISE
)
// Permissions required for BLE scanning on Android 11 and below
val bluetoothLegacyScanPermissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
/**
* Checks if the required BLE scanning permissions are granted.
* On Android 12+, this means BLUETOOTH_SCAN.
* On Android 11 and below, this means ACCESS_FINE_LOCATION.
*
* @param context The application context.
* @return True if all necessary scanning permissions are granted, false otherwise.
*/
fun hasScanPermissions(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31)
bluetoothScanPermissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
} else { // Android 11 and below
bluetoothLegacyScanPermissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
}
/**
* Checks if the required BLE connection permissions are granted.
* On Android 12+, this means BLUETOOTH_CONNECT.
* On Android 11 and below, BLUETOOTH (normal permission) is usually sufficient.
*
* @param context The application context.
* @return True if all necessary connection permissions are granted, false otherwise.
*/
fun hasConnectPermissions(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31)
bluetoothConnectPermissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
} else { // Android 11 and below
// On older versions, BLUETOOTH is a normal permission and granted automatically.
// No runtime request needed for connection.
true
}
}
/**
* Checks if the required BLE advertising permissions are granted.
* On Android 12+, this means BLUETOOTH_ADVERTISE.
* On Android 11 and below, BLUETOOTH (normal permission) is usually sufficient.
*
* @param context The application context.
* @return True if all necessary advertising permissions are granted, false otherwise.
*/
fun hasAdvertisePermissions(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31)
bluetoothAdvertisePermissions.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
} else { // Android 11 and below
// On older versions, BLUETOOTH is a normal permission and granted automatically.
// No runtime request needed for advertising.
true
}
}
/**
* Determines the set of permissions required for BLE scanning based on the current Android version.
*
* @return An array of string permissions.
*/
fun getScanPermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bluetoothScanPermissions
} else {
bluetoothLegacyScanPermissions
}
}
/**
* Determines the set of permissions required for BLE connection based on the current Android version.
*
* @return An array of string permissions.
*/
fun getConnectPermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bluetoothConnectPermissions
} else {
// No runtime permission needed for connect on older Android versions beyond
// the BLUETOOTH normal permission which is auto-granted.
// Returning an empty array or a dummy is fine here if you only check for runtime permissions.
emptyArray()
}
}
/**
* Determines the set of permissions required for BLE advertising based on the current Android version.
*
* @return An array of string permissions.
*/
fun getAdvertisePermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bluetoothAdvertisePermissions
} else {
// No runtime permission needed for advertising on older Android versions beyond
// the BLUETOOTH normal permission which is auto-granted.
emptyArray()
}
}
}
Now, integrate this into your Activity or Fragment:
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Intent
import android.content.pm.PackageManager
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.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
class BleScannerActivity : AppCompatActivity() {
private val TAG = "BleScannerActivity"
private val REQUEST_ENABLE_BT = 1
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
// Register for permission results using the new Activity Result API
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissionsGranted ->
if (permissionsGranted.all { it.value }) {
Log.d(TAG, "All required BLE permissions granted.")
// Permissions granted, proceed with BLE operations (e.g., start scan)
startBleOperations()
} else {
Log.w(TAG, "Not all required BLE permissions were granted.")
Toast.makeText(this, "BLE permissions are required to scan for devices.", Toast.LENGTH_LONG).show()
// Handle denied permissions, perhaps disable BLE features or show a persistent rationale
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.start_scan_button).setOnClickListener {
checkPermissionsAndStartScan()
}
}
private fun checkPermissionsAndStartScan() {
if (!isBluetoothEnabled()) {
promptEnableBluetooth()
return
}
// Determine which permissions are needed based on OS version
val requiredPermissions = BluetoothPermissions.getScanPermissions()
.filter { permission -> // Filter out permissions already granted
ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
if (requiredPermissions.isNotEmpty()) {
// Request permissions if not all are granted
Log.d(TAG, "Requesting BLE permissions: ${requiredPermissions.joinToString()}")
requestPermissionLauncher.launch(requiredPermissions)
} else {
// All necessary permissions are already granted, proceed
Log.d(TAG, "All necessary BLE permissions already granted. Starting BLE operations.")
startBleOperations()
}
}
private fun startBleOperations() {
// Double-check for location services enablement, especially on older Android versions or some OEMs.
// Even with BLUETOOTH_SCAN, location services might need to be ON.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && !isLocationEnabled(this)) {
promptEnableLocationServices()
return
}
// Your BLE scanning logic goes here
Log.i(TAG, "Starting BLE scan...")
Toast.makeText(this, "Starting BLE scan...", Toast.LENGTH_SHORT).show()
// Example: bluetoothAdapter?.bluetoothLeScanner?.startScan(yourScanCallback)
}
private fun isBluetoothEnabled(): Boolean {
return bluetoothAdapter?.isEnabled == true
}
private fun promptEnableBluetooth() {
if (bluetoothAdapter?.isEnabled == false) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) // Use old API for simplicity, or Activity Result API for modern approach
}
}
private fun isLocationEnabled(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// On Android 9 (API 28) and above, use LocationManager
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.isLocationEnabled
} else {
// On older versions, check modes (HIGH_ACCURACY, BATTERY_SAVING, DEVICE_ONLY)
val mode = Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF)
mode != Settings.Secure.LOCATION_MODE_OFF
}
}
private fun promptEnableLocationServices() {
AlertDialog.Builder(this)
.setTitle("Location Services Required")
.setMessage("For BLE scanning on this device, please enable Location Services.")
.setPositiveButton("Enable") { _, _ ->
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivity(intent)
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
Toast.makeText(this, "BLE scanning disabled without Location Services.", Toast.LENGTH_LONG).show()
}
.show()
}
// You would override onActivityResult if using startActivityForResult,
// or integrate into the Activity Result API for enabling Bluetooth.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
Log.d(TAG, "Bluetooth enabled by user.")
checkPermissionsAndStartScan() // Retry permission check and start scan
} else {
Log.w(TAG, "User denied enabling Bluetooth.")
Toast.makeText(this, "Bluetooth must be enabled to use BLE features.", Toast.LENGTH_LONG).show()
}
}
}
}
This comprehensive example demonstrates:
- Conditional Permission Checks: Uses
Build.VERSION.SDK_INTto determine which set of permissions to check and request. - Activity Result API: Modern way to handle permission requests with
registerForActivityResult. - Rationale: Implicitly, if a permission is denied, a
Toastis shown. For a production app, you'd show a more detailed rationale viashouldShowRequestPermissionRationale()before requesting again. - Bluetooth and Location Service Checks: Essential checks for basic Bluetooth adapter state and device-level location services.
Best Practices & Common Gotchas
Navigating Android permissions, especially with BLE, comes with its share of tricky situations. Here are some best practices and common pitfalls to avoid:
-
Over-requesting Permissions:
- Pitfall: Declaring and requesting
BLUETOOTH_SCAN,BLUETOOTH_CONNECT, andBLUETOOTH_ADVERTISEwhen your app only performs, say, scanning. This exposes your app to unnecessary scrutiny from users and increases the chances of denial. - Fix: Only declare and request the specific Bluetooth permissions your app absolutely needs for its current operation. If your app only scans for devices, only request
BLUETOOTH_SCAN. If it later needs to connect, requestBLUETOOTH_CONNECTat that point, perhaps after a successful scan. This aligns with the principle of least privilege. Use theBluetoothPermissionshelper to precisely determine the required permissions.
- Pitfall: Declaring and requesting
-
Forgetting
neverForLocation:- Pitfall: Declaring
BLUETOOTH_SCANin yourAndroidManifest.xmlwithout theandroid:usesPermissionFlags="neverForLocation"attribute. Even withtargetSdk >= 31, the system might still interpret your BLE scanning as a potential source of location information and demandACCESS_FINE_LOCATIONat runtime. - Fix: Always include
android:usesPermissionFlags="neverForLocation"forBLUETOOTH_SCANif your app does not derive location from BLE scans. This is the cornerstone of decoupling BLE from location on Android 12+.
- Pitfall: Declaring
-
Ignoring
targetSdk:- Pitfall: Keeping your
targetSdkbelow 31. If yourtargetSdkis 30 or lower, even if your app runs on Android 12+, the system will not useBLUETOOTH_SCANorBLUETOOTH_CONNECT. Instead, it will still rely onACCESS_FINE_LOCATIONfor scanning and the normalBLUETOOTHpermission for connecting. This leads to the "my app broke on Android 12" scenario if you then try to requestBLUETOOTH_SCAN. - Fix: Update your
targetSdkto 31 (Android 12) or higher as soon as possible. This opts your app into the new permission model and allows you to leverage the granular controls. Incrementing yourtargetSdkrequires careful testing, as it can introduce other behavioral changes.
- Pitfall: Keeping your
-
Inadequate Rationale and UX:
- Pitfall: Prompting for permissions without explaining why they are needed, or failing to guide users when permissions are denied (especially "Don't ask again").
- Fix:
- Before Requesting: Use
shouldShowRequestPermissionRationale()to check if the user has previously denied the permission. If true, display a user-friendly dialog explaining why the permission is crucial for your app's functionality before making the actual permission request. - After Denial (Persistent): If the user denies a critical permission and checks "Don't ask again," your app won't be able to request it again. Detect this state (e.g.,
checkSelfPermissionreturnsPERMISSION_DENIEDandshouldShowRequestPermissionRationalereturnsfalse) and gracefully guide the user to your app's settings page (viaIntent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)) to manually grant the permission. Clear communication here is key to a good user experience.
- Before Requesting: Use
-
Forgetting Location Services (even with
BLUETOOTH_SCAN):- Pitfall: Assuming that
BLUETOOTH_SCANalone is sufficient to enable BLE scanning on all Android 12+ devices. Some OEMs or even specific Android versions (especially prior to S) might still require the device's Location Services to be enabled (the toggle in quick settings), even if your app has theBLUETOOTH_SCANpermission. - Fix: Always include a check for the device's location services status (
LocationManager.isLocationEnabledon API 28+ orSettings.Secure.LOCATION_MODEon older versions) before starting a scan. If location services are off, politely prompt the user to enable them, perhaps with anIntent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).
- Pitfall: Assuming that
Conclusion
The Android 12+ BLE permission changes, while initially challenging, represent a significant step forward for user privacy and control. By understanding the shift from ACCESS_FINE_LOCATION to BLUETOOTH_SCAN/CONNECT, correctly implementing conditional manifest declarations with neverForLocation, and rigorously requesting permissions at runtime, you can ensure your BLE applications are robust and user-friendly. Always prioritize clear user communication and thorough testing across different Android versions to catch these nuanced behavioral changes. Your next step should be to audit your existing BLE applications, update their targetSdk to 31+, and refactor your permission handling to embrace this new, more granular model.
TAGS: android, kotlin, ble, permissions
Top comments (0)