3、Android蓝牙权限与适配:蓝牙相关权限声明、运行时权限请求、Android版本兼容性处理(API 23+、API 31+、API 33+)

Android蓝牙开发中,权限声明与运行时请求是构建稳定应用的第一道门槛。随着Android系统从6.0(API 23)演进到13(API 33),蓝牙权限模型经历了从“安装时授权”到“运行时动态请求”,再到“细粒度蓝牙权限拆分”的重大变革。如果开发者未能正确适配不同API级别的权限规则,轻则导致扫描不到设备、连接失败,重则直接崩溃或上架被拒。本章将系统梳理蓝牙权限的声明规范、运行时请求的完整流程,以及针对API 23+、API 31+、API 33+的兼容性处理策略,并结合实际项目中的典型场景进行案例拆解。

Android蓝牙权限适配决策路径图 蓝牙功能开发 targetSdkVersion ≥ 31 ? 是 API 31+ 蓝牙权限组 BLUETOOTH_SCAN BLUETOOTH_CONNECT 否 API 23-30 传统权限 ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION API 33+ 新增细分 BLUETOOTH_ADVERTISE BLUETOOTH_CONNECT(已有) 无需定位权限(纯BLE) 运行时权限请求 requestPermissions() + 回调 适配要点 ① Manifest声明所有可能用到的权限 ② 运行时按需动态请求 ③ 针对API 31+使用新权限组 ④ API 33+纯BLE无需定位权限 ⑤ 使用ActivityCompat或自定义封装统一处理

上图展示了蓝牙权限适配的核心决策路径。从开发者的目标SDK版本出发,可以快速定位到对应的权限声明与请求策略。接下来,我们将从权限模型演进、声明规范、运行时请求流程、版本兼容处理四个维度展开详细讲解。

3.1 蓝牙权限模型演进与核心原理

Android蓝牙权限体系经历了三个重要阶段,理解其演进逻辑是正确适配的基础。

3.4.1 传统权限模型(API 22及以下)

在Android 6.0之前,蓝牙权限(BLUETOOTH、BLUETOOTH_ADMIN)属于安装时权限。用户安装应用时一次性授予,应用无法在运行时动态请求或撤销。这种模型简单粗暴,但用户对权限的控制力极弱,恶意应用可能滥用蓝牙权限进行数据窃取。

3.4.2 运行时权限模型(API 23 - API 30)

Android 6.0(API 23)引入了运行时权限机制,将权限分为“正常权限”和“危险权限”。蓝牙相关的BLUETOOTH和BLUETOOTH_ADMIN仍属于正常权限,但进行蓝牙扫描和连接时,系统要求应用必须同时拥有定位权限(ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION)。这是因为蓝牙扫描可以间接推断用户位置,属于隐私保护范畴。此阶段的关键适配点:

  • Manifest声明:必须声明BLUETOOTH、BLUETOOTH_ADMIN以及至少一个定位权限。
  • 运行时请求:定位权限属于危险权限,必须在运行时动态请求。
  • 逻辑判断:即使定位权限被拒绝,应用仍可进行已配对设备的通信,但无法扫描新设备。

3.4.3 细粒度蓝牙权限模型(API 31+)

Android 12(API 31)对蓝牙权限进行了重大重构,将原来的BLUETOOTH和BLUETOOTH_ADMIN拆分为三个细粒度权限:

权限名称 对应功能 权限级别
BLUETOOTH_SCAN 扫描蓝牙设备 危险权限(需运行时请求)
BLUETOOTH_CONNECT 连接已配对设备、发起连接 危险权限(需运行时请求)
BLUETOOTH_ADVERTISE 广播蓝牙信号(外围设备模式) 危险权限(需运行时请求)

同时,Android 12+不再强制要求定位权限用于蓝牙扫描(但部分厂商ROM可能仍有此要求)。Android 13(API 33)进一步优化,纯BLE应用(不涉及位置推断)完全无需定位权限。这一变化显著降低了隐私敏感度,但也要求开发者必须精确声明所需权限。

核心原理总结:蓝牙权限适配的本质是“声明-请求-校验”三步骤。声明是静态的,请求是动态的,校验是持续性的。开发者需要根据targetSdkVersion和实际功能选择正确的权限组合,并在运行时处理用户授权结果。

3.2 蓝牙权限声明规范与Manifest配置

无论targetSdkVersion如何,Manifest中的权限声明都是第一步。错误的声明会导致应用安装失败或运行时SecurityException。

3.2.1 基础蓝牙权限声明

<!-- Android 12(API 31)之前的传统声明 -->
<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"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

<!-- 定位权限(API 30及以下必需,API 31+可选) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- 声明蓝牙硬件特性(可选但推荐) -->
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

注意android:usesPermissionFlags="neverForLocation"属性,它向系统声明应用不会使用蓝牙扫描结果推断用户位置。添加此标志后,在Android 12+设备上可以避免强制请求定位权限。但必须确保应用确实不将蓝牙信号用于位置推断,否则可能被下架。

3.2.2 权限声明的最佳实践

  • 冗余声明无害:同时声明新旧权限不会冲突,系统会根据API级别自动选择适用的权限。
  • uses-feature限制:如果应用核心功能依赖BLE,设置required="true"可过滤无BLE硬件的设备。
  • 权限组概念:BLUETOOTH_SCAN和BLUETOOTH_CONNECT属于同一权限组,用户授权其中一个,另一个可能自动授权(但不可依赖此行为)。

风险提示:如果targetSdkVersion ≥ 31,但Manifest中未声明BLUETOOTH_SCAN和BLUETOOTH_CONNECT,应用在调用蓝牙API时会直接抛出SecurityException,且无法通过运行时请求补充声明。务必在发布前检查Manifest合并结果。

3.3 运行时权限请求完整流程

运行时权限请求是蓝牙适配中最容易出错的环节。一个健壮的请求流程需要处理用户授权、拒绝、永久拒绝、权限回调等多个场景。

3.3.1 请求前的状态检查

在发起请求前,必须检查权限是否已授予。Android 6.0+提供了ContextCompat.checkSelfPermission()方法:

private boolean hasBluetoothPermissions() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        return checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
                && checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
    } else {
        return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
    }
}

3.3.2 请求权限的两种方式

方式一:使用ActivityCompat(推荐)

private static final int REQUEST_BLUETOOTH_PERMISSIONS = 1001;

private void requestBluetoothPermissions() {
    String[] permissions;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        permissions = new String[]{
                Manifest.permission.BLUETOOTH_SCAN,
                Manifest.permission.BLUETOOTH_CONNECT
        };
    } else {
        permissions = new String[]{
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
        };
    }
    ActivityCompat.requestPermissions(this, permissions, REQUEST_BLUETOOTH_PERMISSIONS);
}

方式二:使用系统API(API 23+)

requestPermissions(permissions, REQUEST_BLUETOOTH_PERMISSIONS);

两种方式效果相同,但ActivityCompat提供了更好的兼容性封装。

3.3.3 回调处理与用户引导

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_BLUETOOTH_PERMISSIONS) {
        boolean allGranted = true;
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                allGranted = false;
                break;
            }
        }
        if (allGranted) {
            startBluetoothScan();
        } else {
            // 判断是否点击了"不再询问"
            boolean shouldShowRationale = false;
            for (String permission : permissions) {
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
                    shouldShowRationale = true;
                    break;
                }
            }
            if (shouldShowRationale) {
                // 用户拒绝但未勾选"不再询问",可再次请求
                showPermissionRationaleDialog();
            } else {
                // 用户勾选"不再询问",需引导去设置页
                showSettingsDialog();
            }
        }
    }
}

经验提醒:不要在主线程中直接调用requestPermissions(),应确保Activity处于前台且已完全启动。建议在onResume()中检查权限状态,避免在onCreate()中请求导致回调丢失。

3.4 Android版本兼容性处理策略

不同API级别的蓝牙权限差异巨大,一个兼容性良好的应用需要针对不同版本采取差异化策略。

3.4.1 版本差异对照表

功能场景 API 23-30 API 31-32 API 33+
扫描BLE设备 需要定位权限 需要BLUETOOTH_SCAN(可加neverForLocation) 需要BLUETOOTH_SCAN(无需定位)
连接已配对设备 需要BLUETOOTH权限 需要BLUETOOTH_CONNECT 需要BLUETOOTH_CONNECT
作为外围设备广播 需要BLUETOOTH_ADMIN 需要BLUETOOTH_ADVERTISE 需要BLUETOOTH_ADVERTISE
获取已配对设备列表 需要BLUETOOTH权限 需要BLUETOOTH_CONNECT 需要BLUETOOTH_CONNECT

3.4.2 统一权限管理封装

为了简化多版本适配,建议封装一个权限管理工具类:

public class BluetoothPermissionManager {
    public static boolean hasPermissions(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            return hasPermission(context, Manifest.permission.BLUETOOTH_SCAN)
                    && hasPermission(context, Manifest.permission.BLUETOOTH_CONNECT);
        } else {
            return hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION);
        }
    }

    public static String[] getRequiredPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            return new String[]{
                    Manifest.permission.BLUETOOTH_SCAN,
                    Manifest.permission.BLUETOOTH_CONNECT
            };
        } else {
            return new String[]{
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION
            };
        }
    }

    private static boolean hasPermission(Context context, String permission) {
        return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
    }
}

3.4.3 特殊情况处理

  • Android 12+的neverForLocation标志:如果应用声明了此标志,系统会隐藏定位权限请求,但必须确保应用不调用任何可能推断位置的方法(如getScanResults()中的RSSI值)。
  • 部分厂商ROM的兼容问题:某些国产ROM(如MIUI、EMUI)在Android 12基础上仍要求定位权限,建议在运行时检测并提示用户。
  • 权限被撤销的场景:用户可能在系统设置中手动撤销蓝牙权限,应用应在onResume()中重新检查。

风险提示:在Android 12+设备上,如果应用同时声明了BLUETOOTH_SCAN和ACCESS_FINE_LOCATION,系统会弹出两个独立的权限对话框。用户可能只授权其中一个,导致功能异常。建议在UI层面明确提示用户需要同时授权。

3.5 实战案例:多版本兼容的蓝牙扫描功能

下面通过一个完整的案例,展示如何在实际项目中处理蓝牙权限适配问题。

3.5.1 项目背景与约束

某物联网应用需要实现BLE设备扫描功能,目标设备为Android 8.0(API 26)至Android 14(API 34)的机型。应用需要扫描并连接血压计设备,同时作为外围设备广播数据。项目约束:

  • targetSdkVersion = 34(强制要求适配最新权限模型)
  • minSdkVersion = 26(覆盖绝大多数存量设备)
  • 应用不依赖位置信息,纯BLE通信

3.5.2 初始现象与目标

开发初期,测试人员在Android 13设备上发现:点击“扫描”按钮后,应用直接崩溃,Logcat显示SecurityException: Need BLUETOOTH_SCAN permission。而在Android 10设备上,扫描功能正常但无法获取结果。

3.5.3 分析判断过程

通过日志和代码审查,定位到三个核心问题:

  1. Manifest声明缺失:未添加BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限。
  2. 运行时请求逻辑错误:代码中仅请求了定位权限,未根据API级别区分权限集合。
  3. 扫描回调未处理权限拒绝:当用户拒绝权限时,扫描静默失败,无任何提示。

3.5.4 实际操作步骤

步骤1:修正Manifest声明

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<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" />

使用maxSdkVersion="30"限制定位权限仅在API 30及以下生效,避免Android 12+设备弹出不必要的定位请求。

步骤2:重构权限请求逻辑

private void ensurePermissions() {
    if (BluetoothPermissionManager.hasPermissions(this)) {
        startScan();
    } else {
        requestPermissions(BluetoothPermissionManager.getRequiredPermissions(), REQUEST_CODE);
    }
}

步骤3:增强用户引导

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CODE) {
        if (BluetoothPermissionManager.hasPermissions(this)) {
            startScan();
        } else {
            new AlertDialog.Builder(this)
                    .setTitle("需要蓝牙权限")
                    .setMessage("扫描蓝牙设备需要蓝牙权限,请在设置中开启")
                    .setPositiveButton("去设置", (d, w) -> openAppSettings())
                    .setNegativeButton("取消", null)
                    .show();
        }
    }
}

3.5.5 结果对比与复盘

测试设备 适配前 适配后
Android 10(API 29) 扫描无结果(定位权限未请求) 正常扫描并连接
Android 13(API 33) 崩溃(SecurityException) 正常扫描,无定位权限请求
Android 14(API 34) 崩溃 正常扫描,权限流程清晰

复盘经验:蓝牙权限适配的核心在于“版本感知”和“用户引导”。版本感知要求开发者明确每个API级别的权限差异,用户引导则要求应用在权限被拒时提供清晰的解决方案。此外,建议在CI流程中加入多版本真机测试,避免权限问题在发布后才暴露。

3.6 常见误区与总结复盘

在蓝牙权限适配过程中,开发者常陷入以下误区:

  • 误区一:只声明新权限不声明旧权限。如果targetSdkVersion ≥ 31,但minSdkVersion较低,旧设备可能无法识别新权限,导致安装失败。正确做法是同时声明新旧权限。
  • 误区二:忽略neverForLocation标志。不添加此标志,Android 12+设备会强制要求定位权限,增加用户授权负担。
  • 误区三:在Fragment中直接调用requestPermissions。Fragment应使用requestPermissions()方法,但回调需在Fragment中处理,避免与Activity回调混淆。
  • 误区四:认为权限授权后永久有效。用户可以在系统设置中随时撤销权限,应用必须在onResume()中重新检查。
  • 误区五:忽略Android 13+的广告权限。如果应用需要作为外围设备广播,必须单独请求BLUETOOTH_ADVERTISE权限,且该权限与扫描权限相互独立。

总结:Android蓝牙权限适配是一个系统性工程,涉及Manifest静态声明、运行时动态请求、多版本差异化处理三个层面。开发者应建立“版本感知”的编程思维,通过封装工具类统一管理权限逻辑,并在真机上进行多版本验证。记住一个核心原则:权限请求不是一次性操作,而是持续性的状态管理。只有将权限处理融入应用的生命周期管理,才能构建出稳定、合规的蓝牙应用。