4. 位置权限与蓝牙扫描的绑定关系

4.1 绑定原理

蓝牙扫描(特别是 BLE 扫描)在 Android 系统中长期与位置权限绑定,核心原因如下:

  • 信号指纹可定位:蓝牙信标的 MAC 地址、信号强度(RSSI)可被用于三角定位或指纹定位,从而推断用户位置。
  • 隐私保护机制:Google 为防止应用在后台通过蓝牙扫描持续收集用户位置信息,要求扫描操作必须持有位置权限。
  • 合规差异:不同 OEM 厂商对 AOSP 的权限 enforcement 强度不同,但官方 CTS 要求扫描时必须关联位置权限(Android 11 及以下)。

从 Android 10 开始,系统强制要求蓝牙扫描必须伴随位置权限;直到 Android 12 引入 BLUETOOTH_SCAN 细粒度权限,才提供了“仅扫描不定位”的豁免通道。

4.2 权限分类与 API 清单

蓝牙扫描涉及的核心权限及其与位置权限的绑定关系如下表:

权限名称 所属类型 与位置绑定关系 引入 API
ACCESS_FINE_LOCATION 精确定位 完全绑定(Android 10-11 扫描必选) API 1
ACCESS_COARSE_LOCATION 粗略定位 部分绑定(Android 12+ 可与扫描解耦) API 1
BLUETOOTH_SCAN 蓝牙扫描(新权限) 默认不绑定,可声明 neverForLocation 彻底解耦 API 31
BLUETOOTH_ADVERTISE 蓝牙广播 与位置无绑定关系 API 31
BLUETOOTH_CONNECT 蓝牙连接 与位置无绑定关系 API 31
关键区分:ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 属于位置权限组,而 BLUETOOTH_SCAN 属于蓝牙权限组。从 API 31 起,两组权限在语义和运行时授权上完全分离。

4.3 Android 10 / 12 / 13 演变历程

下表梳理了从 Android 9 到 Android 13 蓝牙扫描与位置权限的绑定关系变化:

Android 版本 API 级别 蓝牙扫描权限要求 位置权限是否必需 核心变化
9 及以下 ≤ 28 BLUETOOTH_ADMIN 否 扫描与位置完全解耦
10 – 11 29 – 30 BLUETOOTH_ADMIN 是(必须 ACCESS_FINE_LOCATION) 强制绑定位置,扫描须运行时授权
12 31 – 32 BLUETOOTH_SCAN(新增) 可选(声明 neverForLocation 可免除) 引入细粒度蓝牙权限,提供豁免通道
13+ 33+ BLUETOOTH_SCAN 可选(策略同 API 31) 权限模型稳定,强制使用新权限 API

下面以可视化流程图展示这一演变路径及“仅扫描例外”的定位:

蓝牙扫描与位置权限绑定关系演变 (API 28 → 33) Android 9- API ≤ 28 无需位置权限 Android 10-11 API 29-30 需位置权限 Android 12 API 31-32 新增扫描权限 Android 13+ API 33+ 权限分离完成 仅扫描例外 neverForLocation 无需位置权限 📌 例外规则详解 在 Android 12+(API 31+)通过声明 android:usesPermissionFlags="neverForLocation", 应用可在不获取任何位置权限的前提下发起蓝牙扫描。系统会强制阻止应用 从扫描结果中推断设备位置。此例外仅适用于纯扫描场景,不适用于室内定位等需要位置计算的功能。

4.4 仅扫描例外规则详解

“仅扫描例外”是指:应用只进行蓝牙设备发现或 BLE 扫描,不利用扫描结果计算地理位置时,可以通过特定声明免除位置权限要求。该规则从 Android 12(API 31)开始生效,并延续至 Android 13+。

启用例外的三个必要条件:

  1. 目标 API 等级 ≥ 31;
  2. 在清单中声明 BLUETOOTH_SCAN 权限,并附加 android:usesPermissionFlags="neverForLocation";
  3. 运行时不得将扫描数据用于任何位置推断逻辑(包括 MAC 地址聚类、RSSI 测距、信标定位等)。

清单声明示例:

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
⚠ 重要限制:如果应用同时声明了 BLUETOOTH_SCAN 和 ACCESS_FINE_LOCATION,且 没有 使用 neverForLocation,系统会默认按“需要位置权限”处理。例外声明不能与位置权限共存于同一扫描流程中。

4.5 适配实现流程

以下步骤展示如何基于 API 33 标准适配蓝牙扫描权限,同时兼容 Android 10-12 旧设备:

  1. 清单声明:针对 API 31+ 声明 BLUETOOTH_SCAN + neverForLocation;针对 API 29-30 声明 ACCESS_FINE_LOCATION。
  2. 运行时权限检查:根据 API 级别动态请求相应权限组。
  3. 扫描初始化:使用 BluetoothLeScanner 启动扫描,扫描回调中不处理位置相关数据。
  4. 回退策略:当用户拒绝位置权限时,对 API 29-30 设备禁止扫描;对 API 31+ 设备仍可通过 neverForLocation 路径继续扫描。

运行时权限请求示例(Kotlin):

private fun requestBluetoothScanPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        // Android 12+:只需 BLUETOOTH_SCAN(neverForLocation 已在清单声明)
        requestPermissions(
            arrayOf(Manifest.permission.BLUETOOTH_SCAN),
            RC_BLUETOOTH_SCAN
        )
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Android 10-11:必须请求位置权限
        requestPermissions(
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
            RC_LOCATION
        )
    } else {
        // Android 9-:无需运行时权限
        startBleScan()
    }
}

4.6 调试与验证方法

使用以下 adb 命令验证权限声明是否生效:

# 查看应用的已授予权限列表
adb shell dumpsys package <包名> | grep -E "BLUETOOTH_SCAN|ACCESS_FINE_LOCATION"

# 检查 neverForLocation 标记是否被正确解析
adb shell dumpsys package <包名> | grep "usesPermissionFlags"

日志验证建议:在扫描启动前和扫描回调中分别输出日志,确认未因缺少位置权限而抛出 SecurityException。

// 扫描启动日志
Log.d(TAG, "startBleScan: apiLevel=${Build.VERSION.SDK_INT}, " +
    "hasLocation=${checkSelfPermission(ACCESS_FINE_LOCATION)}")

// 扫描回调异常捕获
try {
    bluetoothLeScanner.startScan(callback)
} catch (e: SecurityException) {
    Log.e(TAG, "扫描被拒绝:缺少必要权限 → ${e.message}")
}

4.7 常见错误与解决方案

  • SecurityException: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION
    原因:Android 10-11 设备未授予位置权限就跑扫描。解决:在 API 29-30 分支强制请求位置权限。
  • neverForLocation 声明被忽略
    原因:清单中同时声明了 ACCESS_FINE_LOCATION 且未加 maxSdkVersion 限制。解决:对 API 31+ 使用 maxSdkVersion="30" 包裹旧位置权限。
  • 扫描返回空结果或立即停止
    原因:系统检测到应用声明了 neverForLocation 但实际代码中调用了位置 API。解决:移除所有与位置相关的库调用,确保扫描结果仅用于设备发现。
  • 商店审核被拒
    原因:声明 neverForLocation 但应用描述或功能暗示会使用位置信息。解决:在隐私政策中明确说明扫描数据不用于位置推断。

4.8 性能与稳定性优化

  • 按需扫描:避免长时间连续扫描,使用 ScanSettings.Builder 设置合理的扫描模式和间隔。
  • 权限缓存:将权限授权状态缓存到内存或 DataStore,避免每次启动扫描都重新检查。
  • 回退降级:当用户拒绝位置权限且设备 API 低于 31 时,优雅提示用户进入设置页手动授权,而非直接崩溃。
  • 测试覆盖:在模拟器上分别测试 API 28、29、31、33 四个镜像,验证权限请求流程和扫描行为是否符合预期。
📐 架构建议:将权限逻辑封装为独立的 BluetoothPermissionManager 单例,内部按 API 级别分发请求流程;业务层只需调用 ensureScanPermission() → startScan() 两个方法,降低耦合。