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 实现流程

BLUETOOTH_SCAN 权限请求流程 应用启动 / 准备扫描 检查 Build.VERSION.SDK_INT ≥ API 31 < API 31 动态请求 BLUETOOTH_SCAN 使用 BLUETOOTH + BLUETOOTH_ADMIN 用户授权 / 拒绝 调用旧版 startLeScan() checkSelfPermission → GRANTED? 是 否 BluetoothLeScanner.startScan() 提示权限说明 / 引导设置 或弹窗后再次请求 旧流程 Android 11 及以下设备 不请求 BLUETOOTH_SCAN

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 并等待用户交互完成后再执行扫描操作,避免生命周期状态丢失导致回调未触发。