4. 蓝牙低功耗扫描权限 BLUETOOTH_SCAN:Android 12+ 新增、声明与动态请求
4.1 原理
Android 12(API 31)将传统 BLUETOOTH 权限拆分为三个细粒度运行时权限,其中 BLUETOOTH_SCAN 专门用于执行蓝牙低功耗(BLE)扫描操作。该权限属于 dangerous 级别,因此不仅需要在清单中声明,还必须在运行时向用户动态请求。
| 属性 | 说明 |
|---|---|
| 权限名称 | android.permission.BLUETOOTH_SCAN |
| 引入 API | Android 12(API 31) |
| 保护级别 | dangerous |
| 功能范围 | 发现 BLE 设备(如扫描广播包) |
| 关联权限 | BLUETOOTH_CONNECT(连接)、BLUETOOTH_ADVERTISE(广播) |
在 Android 12 及更高版本上,如果仅声明旧版 BLUETOOTH 权限而不声明 BLUETOOTH_SCAN,系统将抛出 SecurityException。对于 Android 11 及以下设备,仍需保留 BLUETOOTH 和 BLUETOOTH_ADMIN 权限以保证兼容。
核心概念
BLUETOOTH_SCAN 是一个独立的运行时权限,与位置权限无关(尽管扫描 BLE 可能间接需要位置权限)。如果应用面向 Android 12+,必须显式请求此权限才能执行 BluetoothLeScanner.startScan()。
4.2 实现流程
4.3 声明与动态请求
4.3.1 清单声明
在 AndroidManifest.xml 中同时声明新权限与兼容性权限,确保覆盖所有设备版本:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<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" />
提示
android:maxSdkVersion="30" 确保旧权限不会出现在 Android 12+ 设备的安装清单中,避免冗余权限提示。
4.3.2 运行时动态请求(Kotlin)
使用 ActivityResultLauncher 请求权限,代码更加安全简洁:
class ScanActivity : AppCompatActivity() {
private lateinit var scanLauncher: ActivityResultLauncher<String>
private val requiredPermission = Manifest.permission.BLUETOOTH_SCAN
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
scanLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
startBleScan()
} else {
// 用户拒绝:展示说明或引导设置
showPermissionDeniedDialog()
}
}
}
private fun checkAndRequestScanPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
when {
ContextCompat.checkSelfPermission(this, requiredPermission)
== PackageManager.PERMISSION_GRANTED -> {
startBleScan()
}
shouldShowRequestPermissionRationale(requiredPermission) -> {
// 向用户解释为何需要此权限
showRationaleDialog()
}
else -> {
scanLauncher.launch(requiredPermission)
}
}
} else {
// 旧设备使用 BLUETOOTH + BLUETOOTH_ADMIN
startBleScanLegacy()
}
}
private fun startBleScan() {
// 使用 BluetoothLeScanner 开始扫描
}
}
4.4 调试
通过 adb 模拟授予或撤销权限,便于测试:
# 授予 BLUETOOTH_SCAN 权限
adb shell pm grant com.example.bleapp android.permission.BLUETOOTH_SCAN
# 撤销权限
adb shell pm revoke com.example.bleapp android.permission.BLUETOOTH_SCAN
# 查看应用已授予的权限列表
adb shell dumpsys package com.example.bleapp | grep "permission"
4.5 部署注意事项
- targetSdkVersion 必须为 31+ 才能使用新权限,但
minSdkVersion可保持较低(如 21)。 - 在 Google Play 发布时,如果应用面向 12+ 设备但仅声明旧权限,可能被拒绝或运行时崩溃。
- 若 BLE 扫描用于获取位置,仍需请求
ACCESS_FINE_LOCATION(Android 11 及以下)或ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION(Android 12+ 可选)。
4.6 常见错误
| 错误场景 | 原因 | 解决方案 |
|---|---|---|
| SecurityException: Need BLUETOOTH_SCAN permission | 未声明或未授予 BLUETOOTH_SCAN | 添加声明并运行态请求 |
| 扫描回调为空或异常 | 蓝牙未开启或权限被撤销 | 检查 BluetoothAdapter.isEnabled() |
| 旧设备无法安装 | 误将新权限设为 android:required="true" | 使用 maxSdkVersion 隔离旧权限 |
| 用户永久拒绝后无响应 | 未处理 shouldShowRequestPermissionRationale 返回 false | 引导用户进入系统设置手动授权 |
4.7 性能与稳定性优化
- 权限请求时机:在即将开始扫描时请求,而非应用启动时,降低用户感知负担。
- 缓存授权状态:使用
SharedPreferences记录此前是否被永久拒绝,避免重复弹出无意义的请求。 - 后台扫描限制:Android 12+ 对后台扫描有额外限制,应配合前台服务或高优先级扫描。权限只保证前台扫描合法。
- 线程安全:权限检查与回调始终在主线程执行,不要在其他线程调用
checkSelfPermission。
警告
不要在
不要在
onCreate 中直接调用 requestPermissions。使用 ActivityResultLauncher 并等待用户交互完成后再执行扫描操作,避免生命周期状态丢失导致回调未触发。