2. Android蓝牙权限适配:从Android 6到14的演变之路

蓝牙权限适配,说实话是很多开发者容易踩坑的地方。我记得刚接手一个蓝牙项目时,用户反馈说"扫描不到设备",结果排查了半天——原来是权限没申请对。嗯,今天我们就来彻底捋清楚这件事。

2.1 权限模型的演变:从粗放到精细

Android的蓝牙权限,经历了三个阶段的变化。我把它总结成一张图,方便你理解:

Android蓝牙权限演变时间线 Android 6 ~ 9 BLUETOOTH BLUETOOTH_ADMIN 仅需声明,无需动态申请 位置权限(扫描用) ⚠ 位置权限必须动态申请 Android 10 ~ 11 BLUETOOTH BLUETOOTH_ADMIN ACCESS_FINE_LOCATION (后台扫描需) ⚠ 位置权限要求更严格 Android 12+ BLUETOOTH_SCAN BLUETOOTH_CONNECT BLUETOOTH_ADVERTISE 不再需要位置权限 ✅ 权限更精细

你看,从Android 12开始,Google终于把蓝牙权限从位置权限中剥离出来了。我个人觉得这是最合理的做法——蓝牙就是蓝牙,跟位置有什么关系呢?

2.2 三个新权限详解

Android 12引入了三个新权限,咱们一个一个说清楚。

2.2.1 BLUETOOTH_SCAN

这个权限用来扫描附近的蓝牙设备。说白了,就是调用startScan()时需要的。以前你得同时申请位置权限,现在不用了。

核心要点:

  • 只用于扫描,不用于连接
  • 不需要位置权限(Android 12+)
  • 属于never for location类型

2.2.2 BLUETOOTH_CONNECT

连接设备、配对、传输数据时用这个权限。我遇到过一个问题:有个同事只申请了SCAN权限,结果连接时直接崩溃——嗯,SecurityException没跑。

注意:如果你同时需要扫描和连接,两个权限都得申请。别想着偷懒只申请一个。

2.2.3 BLUETOOTH_ADVERTISE

这个权限是给外围设备用的。如果你的App要作为蓝牙外设广播数据,就需要它。比如做蓝牙信标、或者自定义广播包。

我个人习惯把这三个权限的关系理解成:

  • SCAN = 你看别人
  • CONNECT = 你跟别人握手
  • ADVERTISE = 你让别人看你

2.3 动态权限申请实战

好了,理论说完了,咱们直接上代码。这是我项目中实际在用的方案。

2.3.1 声明权限

首先在AndroidManifest.xml里声明:

<!-- 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" />

<!-- Android 11及以下兼容 -->
<uses-permission android:name="android.permission.BLUETOOTH" 
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" 
    android:maxSdkVersion="30" />

<!-- 位置权限(Android 11及以下扫描需要) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" 
    android:maxSdkVersion="30" />

小技巧:maxSdkVersion="30"限制旧权限,这样在Android 12+上就不会提示用户授权旧权限了。我刚开始没加这个,结果用户说"怎么要这么多权限"...

2.3.2 动态申请代码

这里我封装了一个工具方法,直接复制就能用:

public class BluetoothPermissionHelper {
    
    private static final int REQUEST_CODE_BLUETOOTH = 1001;
    
    /**
     * 获取当前Android版本需要的蓝牙权限列表
     */
    public static String[] getRequiredPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // Android 12+
            return new String[]{
                Manifest.permission.BLUETOOTH_SCAN,
                Manifest.permission.BLUETOOTH_CONNECT,
                Manifest.permission.BLUETOOTH_ADVERTISE
            };
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // Android 10-11
            return new String[]{
                Manifest.permission.BLUETOOTH,
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.ACCESS_FINE_LOCATION
            };
        } else {
            // Android 6-9
            return new String[]{
                Manifest.permission.BLUETOOTH,
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.ACCESS_FINE_LOCATION
            };
        }
    }
    
    /**
     * 检查并申请权限
     */
    public static void checkAndRequestPermissions(Activity activity) {
        String[] permissions = getRequiredPermissions();
        List<String> needRequest = new ArrayList<>();
        
        for (String perm : permissions) {
            if (ContextCompat.checkSelfPermission(activity, perm) 
                != PackageManager.PERMISSION_GRANTED) {
                needRequest.add(perm);
            }
        }
        
        if (!needRequest.isEmpty()) {
            ActivityCompat.requestPermissions(
                activity, 
                needRequest.toArray(new String[0]), 
                REQUEST_CODE_BLUETOOTH
            );
        } else {
            // 权限已全部授予,可以开始蓝牙操作
            startBluetoothWork();
        }
    }
    
    /**
     * 在Activity的onRequestPermissionsResult中调用
     */
    public static void handlePermissionResult(int requestCode, 
            String[] permissions, int[] grantResults) {
        if (requestCode == REQUEST_CODE_BLUETOOTH) {
            boolean allGranted = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    allGranted = false;
                    break;
                }
            }
            if (allGranted) {
                startBluetoothWork();
            } else {
                // 提示用户权限不足
                showPermissionDeniedDialog();
            }
        }
    }
}

2.3.3 权限适配对照表

为了方便查阅,我整理了一张对照表:

功能场景 Android 6-9 Android 10-11 Android 12+
扫描设备 BLUETOOTH + 位置 BLUETOOTH + 位置 BLUETOOTH_SCAN
连接设备 BLUETOOTH BLUETOOTH BLUETOOTH_CONNECT
广播数据 BLUETOOTH_ADMIN BLUETOOTH_ADMIN BLUETOOTH_ADVERTISE
后台扫描 位置 + 后台位置 位置 + 后台位置 BLUETOOTH_SCAN

2.4 避坑指南

做蓝牙开发这些年,我踩过的坑不少。分享几个典型的:

坑1:Android 12上忘记适配新权限

我曾经有个App在Android 11上跑得好好的,升级到Android 12后直接闪退。原因就是没加BLUETOOTH_SCANBLUETOOTH_CONNECT。记住:Android 12上旧的BLUETOOTH权限已经不管用了。

坑2:位置权限的兼容处理

如果你要兼容Android 11及以下设备,位置权限还是得申请。我建议用maxSdkVersion来控制,别一股脑全申请。

我的建议:ApplicationonCreate里判断SDK版本,然后动态决定申请哪些权限。这样代码更干净,用户也少看到弹窗。

2.5 总结

蓝牙权限适配,说白了就是两件事:

  1. 搞清楚你的App需要哪些蓝牙功能(扫描?连接?广播?)
  2. 根据Android版本选择合适的权限组合

我个人习惯在项目初期就把权限适配框架搭好,后面开发就省心了。你想想看,如果等到上线才发现权限问题,那可就麻烦了。

好了,这一章就到这里。记住:权限适配不是难事,但漏掉一个权限,用户可能就直接用不了你的App了。


公众号:蓝海资料掘金营,微信deep3321