1. Android 13+ 蓝牙权限模型变更

从 Android 13(API 33)开始,蓝牙相关权限被重新划分为三个细粒度的运行时权限,原有粗粒度权限 BLUETOOTH、BLUETOOTH_ADMIN 被废弃(在 targetSdk 33+ 的 App 中直接声明将失去运行时授权能力)。这种变化是为了遵循最小权限原则,让用户能够分别控制扫描、连接和广播行为。

1.1 新权限清单与分类

权限名称功能范围是否运行时权限所需定位权限?(Android 10+)
BLUETOOTH_SCAN执行 Bluetooth LE 扫描、获取周边结果是是(除非 neverForLocation 声明)
BLUETOOTH_CONNECT发起或接受连接、配对、通信是否
BLUETOOTH_ADVERTISE发送蓝牙 LE 广播(Peripheral 模式)是否
(废弃)BLUETOOTH旧版蓝牙基础功能(连接、扫描等)仅需声明,
不弹运行时对话框
视具体调用而定
(废弃)BLUETOOTH_ADMIN旧版蓝牙管理(扫描、设置)同上同上
  • 关键变更:在 targetSdk 33 及以上的 App 中,请不要再声明 BLUETOOTH 和 BLUETOOTH_ADMIN,否则系统会忽略它们,导致运行时权限申请失败。
  • 若 App 同时需要扫描结果用于位置相关功能(如发现附近设备),则仍须申请 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION。

1.2 权限分类原理

新权限模型将蓝牙操作拆分为三个互不重叠的“能力域”:

  • 扫描(SCAN):仅涉及被动监听和广告包解析,不产生持久连接。
  • 连接(CONNECT):涉及 GATT、RFCOMM 等连接建立和双向数据交换。
  • 广播(ADVERTISE):设备作为广播者向外发送数据,不需要连接回传。

这种拆分允许系统在权限弹窗中向用户展示明确的操作意图,例如“此应用想要扫描附近的蓝牙设备”。同时,Google 在 Android 13 中引入了 neverForLocation 标志(通过 usesPermissionFlags),当 App 声明 BLUETOOTH_SCAN 时若声明该标志,则可免除对定位权限的依赖(只要不利用扫描结果推断位置)。

实践原则:在 Android 13+ 设备上,优先申请 BLUETOOTH_SCAN(+ 可选的 neverForLocation)、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE,并在 AndroidManifest 中同时兼容旧版本(使用 maxSdkVersion="32" 或 tools:replace 策略)。

1.3 传统权限废弃对升级的影响

如果 App 的 targetSdk 从 32 以下更新到 33+,而未调整权限声明,则会出现以下问题:

  • 所有蓝牙功能权限请求会被系统静默忽略(不弹窗)。
  • 调用 BluetoothAdapter.startDiscovery() 或 BluetoothLeScanner.startScan() 会返回 PERMISSION_DENIED。
  • 应用不会崩溃,但蓝牙功能完全失效。

常见错误:许多开发者仅在 AndroidManifest 中声明 BLUETOOTH_SCAN 等新权限,但忘记删除旧权限或未添加 usesPermissionFlags 标记,导致在部分设备上定位权限仍被要求。

1.4 新权限模型结构图

以下 SVG 展示了 Android 13+ 蓝牙权限的三层模型及与传统权限的对应关系:

Android 13+ 蓝牙权限模型 / 传统权限映射 BLUETOOTH(废弃) BLUETOOTH_ADMIN(废弃) BLUETOOTH_SCAN BLUETOOTH_CONNECT BLUETOOTH_ADVERTISE 扫描权限通常还需要 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION (除非声明 neverForLocation 标志)

1.5 适配第一步:AndroidManifest 声明示例

在 AndroidManifest.xml 中,同时兼容旧版本和 Android 13+ 的声明方式:

<!-- 新权限(Android 13+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

<!-- 旧权限(Android 12 及以下,通过 maxSdkVersion 限制) -->
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="32" />

<!-- 仍需要的定位权限(若扫描需要地理位置) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="32" />

注意:高版本上 maxSdkVersion="32" 确保旧权限不会在 Android 13+ 上被解析,避免冲突。

1.6 性能与稳定性考虑

  • 运行时权限弹窗增加用户交互耗时,应在蓝牙功能真正需要时再请求,避免应用启动时一次性请求所有权限。
  • 如果不需要从扫描结果获取位置,务必添加 android:usesPermissionFlags="neverForLocation",可减少一个定位权限请求,提升用户信赖度。
  • 统一使用 ActivityResultContracts.RequestMultiplePermissions() 或 requestPermissions() 处理权限回调,避免滥用 checkSelfPermission 导致逻辑混乱。

稳定优化要点:在 Android 13+ 上,若用户拒绝某个蓝牙权限,系统不会像定位权限那样频繁提示,因此需要妥善处理拒绝后的降级逻辑(例如提示手动开启)。可在第一次被拒绝后引导用户进入系统设置页面进行授权。