25、权限与蓝牙:BLUETOOTH_SCAN、BLUETOOTH_CONNECT、蓝牙权限变化
蓝牙权限这块,说实话,是Android权限体系里变化最频繁的模块之一。我刚开始做蓝牙开发那会儿,还只有BLUETOOTH和BLUETOOTH_ADMIN两个权限,后来Android 12一出来,直接拆成了BLUETOOTH_SCAN、BLUETOOTH_CONNECT和BLUETOOTH_ADVERTISE。嗯,这里面的坑,我踩过不少。
为什么要把一个权限拆成三个?
说白了,就是权限粒度太粗了。你想想看,一个地图App要扫描蓝牙设备,它其实只需要扫描能力,根本不需要连接设备。但老的权限模型里,你申请了BLUETOOTH,就等于拿到了所有蓝牙操作的通行证。这显然不合理。
Android 12之后,Google把蓝牙权限拆成了三个独立的能力:
- BLUETOOTH_SCAN:只负责扫描周边蓝牙设备
- BLUETOOTH_CONNECT:只负责连接已发现的设备
- BLUETOOTH_ADVERTISE:只负责广播自身(这个用得少)
我个人习惯,在项目里会按最小权限原则来申请。比如一个信标扫描库,我只会申请BLUETOOTH_SCAN,绝不碰BLUETOOTH_CONNECT。
运行时权限的变化要点
这里有个关键变化——位置权限的剥离。在Android 12之前,扫描蓝牙设备必须同时拥有位置权限。但从Android 12开始,如果你只做蓝牙扫描,不再需要位置权限了。
为什么会这样?因为Google引入了BLUETOOTH_SCAN这个新权限,它本身就包含了扫描的合法性。但注意,这有个前提——你的App不能通过蓝牙扫描来推断用户位置。
ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限。别以为有了BLUETOOTH_SCAN就能绕过位置权限,Google Play审核会直接拒绝你的App。
代码实战:如何正确申请蓝牙权限
嗯,这里我直接给出一份我在项目中实际使用的权限申请代码。注意看Android版本的判断逻辑。
// 判断是否需要位置权限
private boolean needsLocationPermission() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
}
// 构建权限列表
private String[] getBluetoothPermissions() {
List<String> permissions = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+:使用新权限
permissions.add(Manifest.permission.BLUETOOTH_SCAN);
permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
} else {
// Android 11及以下:使用旧权限
permissions.add(Manifest.permission.BLUETOOTH);
permissions.add(Manifest.permission.BLUETOOTH_ADMIN);
}
// 如果需要位置信息,添加位置权限
if (needsLocationPermission()) {
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
return permissions.toArray(new String[0]);
}
// 检查权限
private boolean hasBluetoothPermissions() {
for (String permission : getBluetoothPermissions()) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
BLUETOOTH_SCAN只在API 31+有效。
蓝牙权限的声明方式
除了运行时申请,你还需要在AndroidManifest.xml里声明权限。这里有个细节——BLUETOOTH_SCAN和BLUETOOTH_CONNECT是运行时权限,但BLUETOOTH_ADVERTISE是普通权限,不需要运行时申请。
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12+ 新权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- 如果需要位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
注意我加了android:maxSdkVersion="30",这表示位置权限只在Android 11及以下需要。Android 12+不需要位置权限了,这个属性可以帮我们自动屏蔽掉。
权限变化的时间线
我把蓝牙权限的变化整理成了一张表,方便你对照:
| Android版本 | 所需权限 | 是否需要位置权限 | 备注 |
|---|---|---|---|
| Android 6-11 | BLUETOOTH + BLUETOOTH_ADMIN | 是(扫描时需要) | 旧权限模型 |
| Android 12+ | BLUETOOTH_SCAN + BLUETOOTH_CONNECT | 否(仅扫描时) | 新权限模型 |
| Android 12+(定位场景) | BLUETOOTH_SCAN + BLUETOOTH_CONNECT + 位置权限 | 是 | 需要推断位置时 |
避坑指南:我曾经踩过的三个坑
第一个坑:忘记处理权限被拒绝的情况
我曾经在一个蓝牙门禁项目里,用户拒绝了BLUETOOTH_SCAN权限,结果App直接崩溃了。后来我加了个权限回调处理,如果用户拒绝,就弹出一个友好的提示,引导用户去设置里开启。
第二个坑:混淆了扫描和连接
有个同事在代码里只申请了BLUETOOTH_SCAN,但后面调用了connectGatt()方法。结果在Android 12设备上,连接永远失败。记住,扫描和连接是两个独立的权限,缺一不可。
第三个坑:位置权限的过度申请
我见过不少App,明明只是做个蓝牙耳机配对,却申请了ACCESS_FINE_LOCATION。这在Android 12+上完全没必要,而且会让用户觉得你的App在偷窥位置。我的建议是:只申请你真正需要的权限。
知识体系总览
下面这张图,我把蓝牙权限的核心逻辑画出来了。你可以看到,从Android 12开始,权限的申请路径分成了两条:一条是纯蓝牙操作,另一条是涉及位置信息的蓝牙操作。
从这张图可以看得很清楚:Android 12以下,蓝牙权限和位置权限是绑定的;Android 12以上,两者解耦了,但如果你要用蓝牙做定位,还是得申请位置权限。
- Android 12+ 使用 BLUETOOTH_SCAN + BLUETOOTH_CONNECT
- Android 11- 使用 BLUETOOTH + BLUETOOTH_ADMIN + 位置权限
- 纯蓝牙扫描不需要位置权限(Android 12+)
- 蓝牙定位场景仍然需要位置权限
- 权限声明要加 android:maxSdkVersion 做版本控制
好了,蓝牙权限这块的内容就这些。记住,权限管理不是写一次就完事的,每次Android版本更新,你都得回头看看自己的权限逻辑是否还适用。保持更新,保持警惕。