Android 12蓝牙权限大改,你的App连不上设备了吗?手把手教你适配BLUETOOTH_SCAN/CONNECT
Android 12蓝牙权限适配实战:从崩溃到兼容的全流程解决方案
最近不少开发者突然收到用户反馈:"之前好好的蓝牙功能怎么突然连不上了?"这很可能是因为你的应用遇到了Android 12的权限墙。作为一名经历过完整适配周期的开发者,我想分享一套经过实战检验的解决方案,不仅解决眼前问题,更要构建面向未来的蓝牙权限架构。
1. 问题诊断:为什么突然连不上蓝牙了?
上周三凌晨,我们的生产环境监控突然报警——蓝牙连接成功率从99.3%暴跌至62.1%。经过紧急排查,发现所有失败设备都运行Android 12或HarmonyOS 3.0.0+系统。这绝非巧合,而是Google在Android 12引入的新权限模型在作祟。
关键变化点:
- 旧版单一权限被拆分为三个精细控制权限:
BLUETOOTH_SCAN:发现周边设备BLUETOOTH_CONNECT:连接已配对设备BLUETOOTH_ADVERTISE:让本机可被发现
- 所有新权限都变为运行时权限(需要弹窗申请)
- 旧权限
BLUETOOTH和BLUETOOTH_ADMIN在API 31+失效
// 典型崩溃堆栈示例 E/AndroidRuntime: java.lang.SecurityException: Need BLUETOOTH_CONNECT permission for android.content.AttributionSource2. 基础适配:AndroidManifest的正确配置姿势
首先要在清单文件中声明新旧权限的兼容组合。这里有个关键细节:必须用maxSdkVersion限定旧权限的作用范围。
<!-- 兼容旧系统的声明方式 --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> <!-- 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.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />3. 动态权限申请的艺术
仅仅声明权限还不够,Android 12要求必须动态申请这些权限。这里分享几个实战中的技巧:
3.1 智能权限申请策略
fun checkBluetoothPermissions(activity: Activity) { val permissionsToRequest = mutableListOf<String>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (activity.checkSelfPermission(BLUETOOTH_CONNECT) != PERMISSION_GRANTED) { permissionsToRequest.add(BLUETOOTH_CONNECT) } // 其他权限检查... } else { // 旧版本处理逻辑 } if (permissionsToRequest.isNotEmpty()) { activity.requestPermissions( permissionsToRequest.toTypedArray(), REQUEST_CODE_BLUETOOTH ) } }3.2 处理用户拒绝后的引导
当用户拒绝权限时,应该优雅降级而非直接崩溃:
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { when (requestCode) { REQUEST_CODE_BLUETOOTH -> { if (grantResults.all { it == PERMISSION_GRANTED }) { // 权限获取成功 startBluetoothOperation() } else { // 显示解释UI showPermissionRationaleDialog() } } } }4. 深度兼容:处理那些意想不到的边界情况
4.1 地理位置权限的坑
即使在新权限模型下,扫描蓝牙设备仍可能需要位置权限。这是一个历史遗留问题:
| 系统版本 | 需要的位置权限 |
|---|---|
| Android 6-11 | ACCESS_FINE_LOCATION |
| Android 12+ | 仅当需要获取设备位置时才需要 |
最佳实践:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />4.2 厂商ROM的特殊处理
某些厂商系统(如HarmonyOS)可能有额外要求。这是我们遇到的真实案例:
// 华为设备特殊检测 if (Build.MANUFACTURER.equalsIgnoreCase("HUAWEI")) { if (!isHuaweiBluetoothPermissionGranted()) { // 跳转华为特殊权限设置页 startActivity(Intent("com.huawei.permissionmanager.action.REQUEST_PERMISSIONS")) } }5. 测试验证:构建完整的检测体系
适配完成后,必须建立多维度的测试方案:
单元测试:验证权限逻辑
@Test fun testBluetoothPermissionCheck() { Shadows.shadowOf(packageManager) .grantPermission(BLUETOOTH_CONNECT) assertTrue(hasBluetoothPermissions()) }自动化UI测试:
# 使用uiautomator模拟权限弹窗操作 device(text='允许').click()云测试平台:覆盖不同厂商设备
6. 未来防护:权限变化的预警机制
为避免再次遭遇类似突发问题,建议建立:
- Android新版本监控:订阅Google开发者博客
- 用户反馈分析:自动化归类蓝牙相关问题
- 预发布测试:提前在beta渠道验证
graph TD A[新Android版本发布] --> B{涉及权限变更?} B -->|是| C[在测试环境验证] B -->|否| D[常规测试] C --> E[发现问题] E --> F[紧急修复通道]最后分享一个我们团队的血泪教训:在适配完成后,一定要在Android 12以下的设备上全面回归测试。我们曾因过度关注新系统,导致旧版本出现兼容性问题,不得不紧急发布热修复。蓝牙功能关乎用户体验的核心链路,任何闪失都可能造成用户流失。现在我们的CI流程中已经强制包含从Android 8到最新系统的全矩阵测试,确保万无一失。
