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 时若声明该标志,则可免除对定位权限的依赖(只要不利用扫描结果推断位置)。
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+ 蓝牙权限的三层模型及与传统权限的对应关系:
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+ 上,若用户拒绝某个蓝牙权限,系统不会像定位权限那样频繁提示,因此需要妥善处理拒绝后的降级逻辑(例如提示手动开启)。可在第一次被拒绝后引导用户进入系统设置页面进行授权。