Android 12与鸿蒙3.0+蓝牙权限适配实战:从扫描到连接的权限迷宫
1. 蓝牙权限的演变与挑战
记得第一次在Android 12设备上调试蓝牙功能时,我遇到了一个让人抓狂的问题:明明在AndroidManifest.xml里声明了所有看起来相关的权限,但扫描蓝牙设备的代码就是不工作。控制台里那个刺眼的SecurityException让我意识到,Android 12和鸿蒙3.0带来的权限变化,远比想象中复杂。
从Android 6.0开始,系统对权限的管理就在逐步收紧。早期的蓝牙开发只需要在清单文件里声明BLUETOOTH和BLUETOOTH_ADMIN两个权限,就能完成大部分操作。但到了Android 10,事情开始变得不一样——即使有权限,获取到的MAC地址可能只是个虚拟的"02:00:00:00:00:00"。
Android 12彻底重构了蓝牙权限模型,把原来的权限拆分成三个更细粒度的权限:BLUETOOTH_SCAN(扫描)、BLUETOOTH_CONNECT(连接)和BLUETOOTH_ADVERTISE(广播)。这种变化本意是让用户能更精细地控制应用权限,但对开发者来说,意味着要重写大量已经稳定的蓝牙代码。
2. 扫描设备的权限迷宫
2.1 基础权限配置
要在Android 12及以上版本实现蓝牙设备扫描,首先需要在AndroidManifest.xml中声明以下权限:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
这里有个容易踩的坑:即使你的应用只需要扫描而不需要连接设备,BLUETOOTH_CONNECT权限也是必须的。这是因为系统需要确保应用有权限读取扫描结果中的设备信息。
2.2 定位权限的玄机
为什么蓝牙扫描需要定位权限?这其实与蓝牙技术本身的特点有关。通过扫描周围的蓝牙设备,应用可以间接获取用户的位置信息——比如通过识别特定的蓝牙信标。因此,从Android 6.0开始,访问蓝牙扫描结果就需要定位权限。
在代码中,我们需要这样检查并请求权限:
val requiredPermissions = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
if (requiredPermissions.any {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}) {
ActivityCompat.requestPermissions(
this,
requiredPermissions.toTypedArray(),
REQUEST_CODE_BLUETOOTH_PERMISSIONS
)
}
3. 鸿蒙系统的特殊处理
鸿蒙3.0基于Android 12,但在权限处理上有自己的特点。我在实际项目中遇到过这样的情况:同样的代码在Android 12上运行正常,但在鸿蒙设备上却报权限错误。
3.1 华为MDM权限
华为设备可能需要额外的MDM权限才能使用完整的蓝牙功能:
<uses-permission android:name="com.huawei.permission.sec.MDM_BLUETOOTH" />
这个权限通常在企业设备管理场景中使用,普通消费级设备可能不需要。但如果你开发的是一款要在华为企业设备上运行的应用,最好加上这个权限声明。
3.2 鸿蒙的虚拟地址问题
和Android一样,鸿蒙也会返回虚拟MAC地址。要获取真实地址,需要使用华为提供的专属API:
fun getRealBluetoothAddress(): String {
return if (Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true)) {
try {
val clazz = Class.forName("android.os.SystemProperties")
val method = clazz.getMethod("get", String::class.java)
method.invoke(null, "ro.boot.btmacaddr") as? String ?: "02:00:00:00:00:00"
} catch (e: Exception) {
"02:00:00:00:00:00"
}
} else {
BluetoothAdapter.getDefaultAdapter()?.address ?: "02:00:00:00:00:00"
}
}
需要注意的是,这种方法需要系统级权限,普通应用可能无法使用。
4. 动态权限请求的最佳实践
4.1 版本兼容性处理
在实现动态权限请求时,必须考虑不同Android版本的差异:
fun checkBluetoothPermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else {
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
4.2 权限请求的时机
不要在应用一启动就请求所有权限,这会导致用户体验变差。更好的做法是在真正需要使用蓝牙功能时才请求权限,比如当用户点击"扫描设备"按钮时。
scanButton.setOnClickListener {
if (!checkBluetoothPermissions()) {
requestBluetoothPermissions()
} else {
startBluetoothScan()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_BLUETOOTH_PERMISSIONS) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
startBluetoothScan()
} else {
showPermissionDeniedMessage()
}
}
}
4.3 处理用户拒绝权限的情况
用户可能会拒绝授予权限,这时候应该优雅地处理,而不是让应用崩溃或功能完全不可用:
private fun showPermissionDeniedMessage() {
AlertDialog.Builder(this)
.setTitle("需要权限")
.setMessage("蓝牙扫描需要位置和蓝牙权限才能正常工作")
.setPositiveButton("去设置") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
.setNegativeButton("取消", null)
.show()
}
5. 实际扫描实现
5.1 初始化蓝牙适配器
即使有了权限,正确的初始化也很重要:
val bluetoothManager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter == null) {
// 设备不支持蓝牙
return
}
if (!bluetoothAdapter.isEnabled) {
// 可选:请求用户打开蓝牙
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
5.2 实现蓝牙扫描
Android 12对蓝牙扫描API也做了调整:
private val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
// 处理扫描结果
}
override fun onScanFailed(errorCode: Int) {
// 处理扫描失败
}
}
fun startBluetoothScan() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val filters = listOf<ScanFilter>() // 可以添加过滤条件
bluetoothLeScanner.startScan(filters, scanSettings, scanCallback)
} else {
// 旧版本实现
bluetoothAdapter.startLeScan(leScanCallback)
}
}
fun stopBluetoothScan() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bluetoothLeScanner.stopScan(scanCallback)
} else {
bluetoothAdapter.stopLeScan(leScanCallback)
}
}
5.3 后台扫描的限制
Android 12对后台蓝牙扫描增加了额外限制。如果应用在后台运行,需要声明BLUETOOTH_SCAN权限的usesPermissionFlags属性:
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
这个标志告诉系统,你的应用不会使用蓝牙扫描结果来获取位置信息。如果你确实需要位置信息,则还需要声明ACCESS_BACKGROUND_LOCATION权限。
6. 测试与调试技巧
6.1 使用adb测试权限
在开发过程中,可以使用adb命令快速测试权限状态:
adb shell pm list permissions -g
adb shell dumpsys package your.package.name | grep permission
6.2 处理不同厂商的差异
不同厂商的Android设备可能有不同的蓝牙实现。特别是在华为、小米等设备上,可能需要额外的权限或特殊处理。最好的办法是准备多台不同厂商的设备进行测试。
6.3 日志记录
完善的日志记录能帮助快速定位权限问题:
fun logPermissionStatus(permission: String) {
val status = if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
"GRANTED"
} else {
"DENIED"
}
Log.d("PermissionCheck", "$permission: $status")
}
7. 常见问题解决方案
7.1 权限已授予但功能仍不可用
有时候明明已经授予了权限,但蓝牙功能仍然报错。这可能是因为:
- 应用缓存问题 - 尝试清除应用数据或重启设备
- 权限被系统限制 - 某些厂商ROM会限制后台权限
- 缺少依赖权限 - 比如忘记声明
BLUETOOTH_CONNECT
7.2 在Android 10上获取虚拟地址
这是正常行为,从Android 10开始,非特权应用无法获取真实蓝牙MAC地址。如果必须获取真实地址,可以考虑:
- 使用设备厂商提供的特殊API
- 让用户手动输入
- 改用其他设备识别方式
7.3 鸿蒙设备上的兼容性问题
鸿蒙虽然基于Android,但在某些实现上有所不同。如果遇到奇怪的问题,可以:
- 检查是否使用了华为专属API
- 确认鸿蒙版本是否完全兼容对应的Android API级别
- 查阅华为开发者文档获取鸿蒙专属指导
在经历了无数次调试和崩溃后,我总结出一个蓝牙开发的金科玉律:永远不要假设权限已经授予,永远做好回退方案,永远测试不同厂商的设备。蓝牙功能看似简单,但在现代Android系统上,正确处理权限是确保功能可用的第一步。
更多推荐

所有评论(0)