第3章:Android 12新增的BLUETOOTH_SCAN权限——作用范围、声明方式、与位置权限的关联

3.1 概念解释:为什么需要独立的蓝牙扫描权限?

在 Android 11 及之前版本,执行蓝牙扫描仅需要 BLUETOOTH 权限(普通权限,安装时自动授予)以及 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 运行时权限。这导致一个问题:用户为了连接蓝牙耳机,却不得不授予位置权限,因为系统将蓝牙扫描获得的设备 MAC 地址视为一种“位置信息”。为了彻底隔离位置隐私与蓝牙连接功能,Android 12 引入了三个细粒度蓝牙权限:BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE、BLUETOOTH_CONNECT。本章聚焦 BLUETOOTH_SCAN 权限。

权限名称类别作用授权方式
BLUETOOTH_SCAN运行时权限(dangerous)执行蓝牙 LE 或经典蓝牙扫描,获取设备列表运行时弹窗(可随时撤销)
BLUETOOTH_CONNECT运行时权限发起或接受蓝牙连接运行时弹窗
BLUETOOTH_ADVERTISE运行时权限广播蓝牙 LE 广告包运行时弹窗
ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATION运行时权限部分场景仍需位置权限(见下文)运行时弹窗

3.2 作用范围:什么操作需要 BLUETOOTH_SCAN?

BLUETOOTH_SCAN 覆盖以下系统 API 调用:

  • BluetoothAdapter.startLeScan()(已废弃但兼容)
  • BluetoothLeScanner.startScan()
  • BluetoothAdapter.startDiscovery()(经典蓝牙发现)
  • 任何获取扫描结果回调的方法

该权限不包含连接或配对功能。如果应用只需扫描附近蓝牙设备并显示列表(例如设备扫描仪、信标检测),则只需 BLUETOOTH_SCAN + BLUETOOTH_ADMIN(普通权限)即可,不需要位置权限。

3.3 与位置权限的关联:何时仍需位置权限?

关键原则:如果应用从扫描结果中提取蓝牙设备的 MAC 地址并用于地理定位,或虽然不读取 MAC 地址但系统根据返回信息能反推位置,则仍然需要位置权限。 具体分三种场景:

场景A:纯设备发现,不获取任何硬件标识符

使用 ScanCallback 的 onScanResult 时,不调用 ScanResult.getDevice().getAddress(),只使用名称、RSSI、服务 UUID。此时 不需要 位置权限。
但在 Android 12 上,如果 targetSdkVersion = 31+,系统会默认抑制 MAC 地址的返回(返回 02:00:00:00:00:00 或类似固定值),所以实际上开发者无法获取真实 MAC。

场景B:需要真实 MAC 地址(例如 MAC 绑定、设备白名单)

必须同时声明 ACCESS_FINE_LOCATION 且在运行时申请。系统才会返回真实 MAC。这意味着如果应用想实现“扫描并过滤特定蓝牙耳机”,仍需位置权限。

场景C:使用蓝牙扫描进行室内定位或接近检测

例如基于蓝牙信标的商城导航、防丢器距离估算。此时蓝牙扫描结果直接用于位置计算,必须同时拥有 BLUETOOTH_SCAN + ACCESS_FINE_LOCATION。并且 Android 12 对后台扫描的管控更严格(需要 ACCESS_BACKGROUND_LOCATION)。

3.4 声明方式:清单文件 + 运行时请求

在 AndroidManifest.xml 中声明权限:

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 如果需要 MAC 地址 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

注意 BLUETOOTH_ADMIN 是普通权限(自动授予),但声明它以便调用扫描相关方法。运行时请求示例:

private static final int REQUEST_CODE_BLUETOOTH_SCAN = 1001;

private void requestBluetoothScanPermission(Activity activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_SCAN)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(activity,
                    new String[]{Manifest.permission.BLUETOOTH_SCAN},
                    REQUEST_CODE_BLUETOOTH_SCAN);
        }
    }
}

3.5 类比:蓝牙扫描权限就像“敲门许可”

  • BLUETOOTH_SCAN = 你站在门外,允许你敲门并听里面是否有回应(发现设备)。
  • BLUETOOTH_CONNECT = 门打开了,你被允许进入(建立连接)。
  • ACCESS_FINE_LOCATION = 你敲门时还要记下门牌号(获取 MAC 地址),进而知道具体位置。

旧的权限模型要求你同时说“我要敲门”并且“我要看门牌号”,即使你只是敲门。新模型解耦了这两种意图。

3.6 原理图解:权限依赖与扫描流程

下图展示 Android 12 上蓝牙扫描的权限检查路径与数据流。

Android 12 蓝牙扫描权限决策流程 应用发起扫描 是否声明 BLUETOOTH_SCAN? 扫描失败(SecurityException) 用户已授予? 弹窗申请 或 失败 蓝牙扫描开始 只获取名称/RSSI 需 ACCESS_FINE_LOCATION 说明:如果应用仅使用 onScanResult 中的 name / RSSI 则无需位置权限。

3.7 常见误区

  1. 误区:声明了 BLUETOOTH_SCAN 就不需要 BLUETOOTH_ADMIN
    正解:BLUETOOTH_ADMIN 是调用 startLeScan() 等方法的普通权限(非 runtime),仍需在 manifest 中声明。
  2. 误区:只要 targetSdkVersion=31 以上,所有蓝牙扫描都无需位置权限
    正解:只有当你放弃获取真实 MAC 地址时才无需位置权限。如果你的业务逻辑依赖 MAC(例如配对历史),位置权限仍是必需的。
  3. 误区:在 Android 12 上运行时申请 BLUETOOTH_SCAN 后,所有蓝牙扫描都能成功
    正解:系统仍会检查蓝牙是否已开启、是否满足其他硬件约束。权限仅解决访问控制问题。

3.8 练习题(自测理解)

题目1: 一个信标检测 App 只需监听 iBeacon 广播并显示设备名称、距离(RSSI 估算),不读取 MAC 地址。在 Android 12 上最小需要哪些权限?

题目2: 以下代码片段在 Android 12(targetSdk=31)上运行,会抛出 SecurityException 吗?

ScanCallback callback = new ScanCallback() {
    @Override public void onScanResult(int callbackType, ScanResult result) {
        String address = result.getDevice().getAddress(); // 获取 MAC
    }
};
BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner();
scanner.startScan(callback);

题目3: 实现一个蓝牙解压球(Sphero)连接 App,需要扫描并连接特定 MAC 的设备。需要在清单中声明哪几个权限?运行时需申请哪几个?

答案提示: 题目1:BLUETOOTH_SCAN + BLUETOOTH_ADMIN。题目2:会抛出 SecurityException,因为未申请 ACCESS_FINE_LOCATION 却试图获取 MAC 地址。题目3:清单声明 BLUETOOTH_SCAN、BLUETOOTH_ADMIN、ACCESS_FINE_LOCATION;运行时需申请 BLUETOOTH_SCAN 和 ACCESS_FINE_LOCATION。

3.9 应用边界:何时应考虑降级策略?

如果应用需要同时兼容 Android 11 及以下设备,必须在代码中判断 SDK 版本:

  • API < 31:使用 BLUETOOTH + ACCESS_FINE_LOCATION。
  • API ≥ 31:使用 BLUETOOTH_SCAN + 按需位置权限。

推荐在 AndroidManifest.xml 中声明新旧两套权限,系统会根据 SDK 版本忽略不相关的权限声明。但需注意:如果旧设备看到 BLUETOOTH_SCAN 会忽略(不提示),反之亦然。