1. 蓝牙权限的演变与挑战

记得第一次在Android 12设备上调试蓝牙功能时,我遇到了一个让人抓狂的问题:明明在AndroidManifest.xml里声明了所有看起来相关的权限,但扫描蓝牙设备的代码就是不工作。控制台里那个刺眼的SecurityException让我意识到,Android 12和鸿蒙3.0带来的权限变化,远比想象中复杂。

从Android 6.0开始,系统对权限的管理就在逐步收紧。早期的蓝牙开发只需要在清单文件里声明BLUETOOTHBLUETOOTH_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 权限已授予但功能仍不可用

有时候明明已经授予了权限,但蓝牙功能仍然报错。这可能是因为:

  1. 应用缓存问题 - 尝试清除应用数据或重启设备
  2. 权限被系统限制 - 某些厂商ROM会限制后台权限
  3. 缺少依赖权限 - 比如忘记声明BLUETOOTH_CONNECT

7.2 在Android 10上获取虚拟地址

这是正常行为,从Android 10开始,非特权应用无法获取真实蓝牙MAC地址。如果必须获取真实地址,可以考虑:

  1. 使用设备厂商提供的特殊API
  2. 让用户手动输入
  3. 改用其他设备识别方式

7.3 鸿蒙设备上的兼容性问题

鸿蒙虽然基于Android,但在某些实现上有所不同。如果遇到奇怪的问题,可以:

  1. 检查是否使用了华为专属API
  2. 确认鸿蒙版本是否完全兼容对应的Android API级别
  3. 查阅华为开发者文档获取鸿蒙专属指导

在经历了无数次调试和崩溃后,我总结出一个蓝牙开发的金科玉律:永远不要假设权限已经授予,永远做好回退方案,永远测试不同厂商的设备。蓝牙功能看似简单,但在现代Android系统上,正确处理权限是确保功能可用的第一步。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐