Android 工业手持机蓝牙扫描失败:为什么同时需要蓝牙和定位权限

148 阅读10分钟

Android 10 工业手持机蓝牙扫描失败:为什么需要定位权限

本文记录一次 GP 蓝牙打印机在 Android 10 工业手持机上无法扫描的排查过程。最终结论是:Android 10 的标准蓝牙权限模型要求 App 在主动扫描设备时获得位置权限;现场授予“位置”权限后,扫描立即恢复。Android 12+ 的“附近的设备”权限属于另一套权限模型,需要分开讨论。

一、问题现象

现场的 GP-Print 页面显示:

扫描失败
无法启动蓝牙扫描

当时已经确认:

  • 手持机蓝牙已打开;
  • 系统蓝牙设置页能搜索到附近设备;
  • 打印机已开机且处于可发现状态;
  • 同一版本的 App 在开发人员的普通 Android 设备上无法复现,扫描速度正常;
  • 杀掉 App 进程并重新打开后,问题仍然存在。

最终,现场将 App 的“位置”权限打开后,蓝牙扫描立即恢复正常。

二、报错实际发生在哪里

该项目使用 Android 经典蓝牙发现来查找打印机。原生模块会先检查蓝牙适配器和权限,然后调用:

val started = bluetoothAdapter?.startDiscovery() == true

startDiscovery() 返回 false 时,App 显示“无法启动蓝牙扫描”。这里的含义不是“没有找到打印机”,而是“扫描未能启动”。蓝牙状态、权限、正在进行的配对、已有排队请求或蓝牙栈异常都可能导致启动失败,不能仅凭这个返回值断定具体原因。

相关代码位于:

三、为什么蓝牙扫描历史上会和定位权限有关

蓝牙扫描本身不会直接读取 GPS 坐标,但扫描结果会暴露周围设备的标识和信号强度。如果将这些信息与已知位置的蓝牙信标、商场设备或其他数据库结合,理论上可以推断用户所在位置。

因此,在 Android 11(API 30)及以下版本中,扫描蓝牙设备需要 ACCESS_FINE_LOCATION 运行时权限。Android 10 对应 API 29,正好适用这项标准要求,并不是工业手持机特有的兼容行为。Android 官方蓝牙权限文档也明确说明,旧版 Android 要求该权限,是因为蓝牙扫描可能被用于收集位置信息。

四、Android 12 后的标准权限模型

Android 12(API 31)开始引入了专门的附近设备权限:

权限用途
BLUETOOTH_SCAN搜索附近的蓝牙设备
BLUETOOTH_CONNECT连接或读取已配对蓝牙设备的信息
BLUETOOTH_ADVERTISE让当前设备对外广播

这些都是运行时权限,不能只写在 AndroidManifest.xml 中,App 还必须在运行时向用户申请。Android 会将其展示为“附近的设备”权限。Android 12 行为变更文档也将这套权限作为新的蓝牙访问模型。

按照标准 Android 规则,如果 App 不使用蓝牙扫描结果推导用户位置,可以在 BLUETOOTH_SCAN 上声明:

<uses-permission
    android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />

在符合标准的 Android 12+ 设备上,这种场景通常只需要“附近的设备”权限,不应再强制要求定位权限。需要注意的是,Android 官方同时说明,使用 neverForLocation 可能导致某些 BLE 信标被过滤。

五、为什么这台 Android 10 手持机需要位置权限

这里必须先根据系统版本判断适用哪套权限模型。现场手持机运行 Android 10,不存在 Android 12 才引入的“附近的设备”运行时权限。

本次现场结果是:

权限状态手持机实际结果
蓝牙开启,App 未获得位置权限startDiscovery() 失败
蓝牙开启,App 获得精确位置权限扫描立即恢复

这个结果符合 Android 10 的标准权限模型:

BLUETOOTH / BLUETOOTH_ADMIN(Manifest 普通权限)
                         +
ACCESS_FINE_LOCATION(运行时权限)
                         ↓
                  允许扫描蓝牙设备

因此,本次事件本身不能证明该手持机使用了厂商定制的“混合权限模型”,也不能证明 Android 12+ 工业手持机必然需要额外的位置权限。如果要得出后一结论,需要在 Android 12+ 设备上单独做权限对照试验。

Android 10 中相关权限分别解决什么问题

  • BLUETOOTH / BLUETOOTH_ADMIN:允许 App 使用传统蓝牙并启动设备发现;在 Android 10 上属于 Manifest 权限,不会显示为 Android 12 的“附近的设备”运行时授权框。
  • ACCESS_FINE_LOCATION:满足 Android 10 对蓝牙扫描的隐私保护要求。App 获得该权限不代表业务实际读取 GPS 坐标或收集用户位置。

六、为什么系统设置能搜到,App 却搜不到

系统蓝牙设置页是系统级应用,它可以通过系统签名或特权直接访问蓝牙服务。普通 App 则运行在独立沙箱中,必须通过 Manifest 声明、运行时授权和系统的 AppOps 检查。

所以,“系统设置可以搜到”只能证明:

  • 蓝牙硬件基本正常;
  • 打印机正在广播且距离合适。

它不能证明普通 App 的权限和扫描调用一定正常。

七、为什么重启 App 没有作用

权限状态由 Android 系统持久保存,不会因为杀掉 App 进程而改变。同样,异常的蓝牙系统服务状态也不一定会随 App 退出而重置。

所以排查时应该区分:

  • 权限问题:到“设置 → 应用 → 当前 App → 权限”中修改;
  • 蓝牙栈状态问题:关闭蓝牙,等待数秒后重新打开,必要时重启整台手持机;
  • App 进程问题:只有当 App 内部保留了错误状态时,重启 App 才可能有效。

八、项目当前实现及版本边界

1. Manifest 声明

项目已在 AndroidManifest.xml 中同时声明旧版和新版蓝牙权限:

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

<!-- Android 12+ -->
<uses-permission
    android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

neverForLocation 表示业务不使用蓝牙扫描结果推导物理位置。按照 Android 官方的最小权限建议,如果 Android 12+ 不需要位置权限,旧版权限可以增加 android:maxSdkVersion="30"

<uses-permission
    android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
<uses-permission
    android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />
<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="30" />

项目当前没有给 ACCESS_FINE_LOCATION 设置版本上限,并在 Android 12+ 也申请它。这是一项偏保守的兼容实现,但本次 Android 10 现场事件不能作为其必要性的证据。后续应在目标 Android 12+ 工业设备上验证后,决定保留、按厂商/型号申请,或限制到 API 30。

2. 扫描前动态申请权限

项目当前按系统版本申请权限:Android 11 及以下只动态申请精确位置;Android 12+ 申请附近设备权限,并额外申请精确位置作为兼容措施。

const permissions = Number(Platform.Version) >= 31
  ? [
      PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
      PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
      PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
    ]
  : [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];

const results = await PermissionsAndroid.requestMultiple(permissions);

如果用户选择“不再询问”,App 会提供“前往设置”入口,避免用户只看到扫描失败却不知道如何恢复。

3. 原生层保留防御性检查

即使 React Native 页面已经申请权限,原生模块仍应在调用蓝牙 API 前检查:

if (ActivityCompat.checkSelfPermission(
        ctx,
        Manifest.permission.ACCESS_FINE_LOCATION
    ) != PackageManager.PERMISSION_GRANTED
) {
    promise.reject(
        "PERMISSION_DENIED",
        "缺少定位权限,无法扫描蓝牙设备"
    )
    return
}

这样可以避免页面遗漏申请、权限在运行期被收回,或其他页面直接调用原生模块时出现不明确的扫描失败。需要注意,当前原生层会在所有 Android 版本上强制检查精确位置权限;如果后续决定在标准 Android 12+ 设备上采用最小权限方案,这里也应同步改为按系统版本或设备兼容策略检查。

4. 兼容取消扫描的异步时序

部分手持机上,cancelDiscovery() 不会同步释放蓝牙扫描状态。如果紧接着再次调用 startDiscovery(),也可能返回 false

本次同时增加了两层保护:

  • JavaScript 层停止扫描后等待 600ms;
  • 原生层启动失败时等待 700ms 并自动重试一次。

这个时序修复不是本次“定位权限”问题的最终触发原因,但可以减少工业设备上连续扫描导致的偶发失败。

5. 权限提示支持多语言

新增的权限和扫描错误提示已接入项目 i18n,支持简体中文、英文、日文、越南文和孟加拉文。“确定”、“取消”、“前往设置”等按钮继续复用 common 命名空间,避免在蓝牙模块重复维护通用文案。

九、现场排查清单

遇到类似问题时,建议按以下顺序排查:

  1. 确认打印机已开机并处于可发现状态。
  2. 确认手持机系统蓝牙已打开。
  3. Android 12+:检查 App 的“附近的设备”权限。
  4. Android 11 及以下:检查 App 的“位置”权限;Android 10 应授予精确位置权限。
  5. 如果权限已授予仍无法扫描,确认系统“定位”总开关已打开;不同 Android 版本和厂商 ROM 的实际限制可能不同。
  6. 关闭蓝牙,等待 5–10 秒后重新打开。
  7. 如果系统可以发现打印机,可先在系统中完成配对,再回到 App 连接。
  8. 仍然失败时,采集 GPrinterBluetoothAdapterReactNativeJS 日志。

可使用以下命令检查现场设备:

adb shell getprop ro.build.version.release
adb shell getprop ro.build.version.sdk

adb shell dumpsys package com.matsuokaphone \
  | grep -E "BLUETOOTH_SCAN|BLUETOOTH_CONNECT|ACCESS_FINE_LOCATION"

adb logcat -c
adb logcat -v time GPrinter:D BluetoothAdapter:D BluetoothManagerService:D ReactNativeJS:V '*:S'

十、方案边界与后续优化

对于 Android 10,主动扫描蓝牙设备时申请精确位置权限是系统标准要求。对于严格遵循 Android 12+ 规范的设备,如果已经声明 neverForLocation 且业务不使用扫描结果推导位置,仅为连接打印机而申请位置权限通常不是最小权限方案。

项目当前在 Android 12+ 同时申请“附近的设备”和位置权限,但应将它视为待验证的兼容策略,而不是本次 Android 10 事件已经证实的结论。

如果未来需要发布到更广泛的消费设备,可进一步考虑:

  • 根据设备厂商、型号或系统版本按需申请定位权限;
  • 优先展示系统已配对的打印机,减少主动扫描;
  • 评估 Android CompanionDeviceManager,由系统代理伴生设备配对;
  • 将“权限被拒绝”、“系统定位未开启”和“蓝牙栈拒绝扫描”拆分为不同错误码。

结论

本次问题不是打印机故障,也不是手持机蓝牙硬件损坏。现场手持机运行 Android 10,而 Android 10 的标准权限模型要求 App 获得精确位置权限后才能正常扫描蓝牙设备。

一句话概括:

Android 10 扫描蓝牙设备需要位置权限;Android 12+ 才使用“附近的设备”权限。两者是不同 Android 版本下的标准权限模型,不能混为同一次厂商兼容问题。

参考资料