Flutter 3.47 鸿蒙三方库适配实战:permission_handler 权限兼容层

一、项目信息

项目内容
适配仓库名称permission_handler
原始包名permission_handler
适配版本13.0.2-ohos.1
上游项目baseflow/flutter-permission-handler
方法通道flutter.baseflow.com/permissions/methods
OHOS 原生入口PermissionHandlerPlugin
构建产物entry-default-unsigned.hap

权限插件适配最容易出现的错误不是“请求失败”,而是“把不支持的权限伪装成已授权”。permission_handler 的 OHOS 版本因此采用保守策略:只映射系统有明确对应关系的权限,未映射项统一返回 denied。

二、保留上游 Dart 契约

业务代码继续使用原来的 Permission 对象:

final PermissionStatus status = await Permission.camera.status;
final PermissionStatus result = await Permission.camera.request();

顶层包导出原有 API:

Future<bool> openAppSettings();

abstract class Permission {
  Future<PermissionStatus> get status;
  Future<PermissionStatus> request();
  Future<bool> get isGranted;
  Future<ServiceStatus> get serviceStatus;
}

OHOS 适配没有要求业务页面新增 PermissionOhos 类型。只要宿主工程声明了对应 OHOS 权限,Dart 调用就会通过 MethodChannel 进入 PermissionHandlerPlugin。

三、权限名称映射

ArkTS 插件使用 permissionName() 将 Dart 权限枚举转换为 OHOS 权限名:

private permissionName(value: number): string | null {
  switch (value) {
    case 1:
      return 'ohos.permission.CAMERA';
    case 2:
      return 'ohos.permission.READ_CONTACTS';
    case 3:
    case 5:
      return 'ohos.permission.APPROXIMATELY_LOCATION';
    case 4:
      return 'ohos.permission.LOCATION_IN_BACKGROUND';
    case 7:
    case 14:
      return 'ohos.permission.MICROPHONE';
    case 13:
      return 'ohos.permission.RECEIVE_SMS';
    case 17:
      return 'ohos.permission.NOTIFICATION_CONTROLLER';
    case 22:
    case 28:
    case 29:
    case 30:
      return 'ohos.permission.DISCOVER_BLUETOOTH';
    default:
      return null;
  }
}

映射时不能只看权限名称相似。例如 Permission.locationAlways 在 OHOS 上需要后台定位权限,不能复用前台定位权限;蓝牙的扫描、连接和广播能力也要按 OHOS 权限模型分别处理。

四、请求与状态检查

状态检查使用 AccessToken:

private async checkPermission(value: number): Promise<number> {
  const name = this.permissionName(value);
  if (name === null || this.context === null) {
    return 0;
  }

  const status = abilityAccessCtrl.createAtManager().checkAccessTokenSync(
    this.tokenId(),
    name as Permissions,
  );
  return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED ? 1 : 0;
}

请求权限时逐个调用系统接口:

const response = await abilityAccessCtrl.createAtManager()
  .requestPermissionsFromUser(this.context, [name as Permissions]);

const granted = response.authResults.length > 0 &&
  response.authResults[0] ===
    abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
return granted ? 1 : 0;

这里使用单权限数组是为了让每个 Permission 的结果保持独立,避免一个权限组的授权结果污染另一个权限。

五、宿主工程必须声明权限

OHOS 权限不能只在 Dart 层请求,宿主 module.json5 也必须声明。相机和麦克风示例:

"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:camera_permission_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:microphone_permission_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

对应的 string.json:

{
  "string": [
    {
      "name": "camera_permission_reason",
      "value": "用于扫描二维码和拍摄照片"
    },
    {
      "name": "microphone_permission_reason",
      "value": "用于录制语音消息"
    }
  ]
}

六、当前不支持的能力

openAppSettings() 当前明确返回 false:

case 'openAppSettings':
  result.success(false);
  return;

shouldShowRequestPermissionRationale() 当前返回 false,业务应自行在请求前解释权限用途。checkServiceStatus() 返回固定的未适用状态,因为 OHOS 没有与旧接口完全一致的服务开关。

这些限制必须让业务代码可见。若把 openAppSettings() 返回 true,调用方会以为已经跳转设置页,最终却看不到任何页面。

七、构建与模拟器验证

验证命令:

flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze
flutter build hap --debug --no-codesign

当前结果:

检查项结果
根目录 flutter analyze通过
示例目录 flutter analyze通过
OHOS ArkTS 编译通过
HAP 产物entry-default-unsigned.hap
release HAP 大小约 22 MB
页面状态Baseflow 权限列表正常显示

模拟器截图:

在这里插入图片描述

八、权限适配的工程化验证

1. 当前映射的完整语义

OHOS 权限名不是简单按字符串替换得到的。当前适配把上游枚举值映射到系统权限:

上游枚举值OHOS 权限说明
0, 36, 37ohos.permission.READ_CALENDAR日历读取及上游相关变体
1ohos.permission.CAMERA相机
2ohos.permission.READ_CONTACTS联系人读取
3, 5ohos.permission.APPROXIMATELY_LOCATION近似位置
4ohos.permission.LOCATION_IN_BACKGROUND后台定位
7, 14ohos.permission.MICROPHONE麦克风
8ohos.permission.READ_CALL_LOG通话记录读取
9, 10ohos.permission.READ_IMAGEVIDEO图片和视频读取
12ohos.permission.READ_HIVIEW_DFX系统日志读取
13ohos.permission.RECEIVE_SMS接收短信
15, 23ohos.permission.READ_WRITE_USER_FILE用户文件读写
17ohos.permission.NOTIFICATION_CONTROLLER通知控制
22, 28, 29, 30ohos.permission.DISCOVER_BLUETOOTH蓝牙发现

未出现在表中的权限不会被猜测映射,而是返回 PermissionStatus.denied。这是兼容层的安全边界:宁可让业务显示“需要授权”,也不能把未知权限报告为已授权。

2. 状态检查为什么需要 AccessToken

OHOS 的权限状态不能只凭权限名判断,必须知道当前应用的 AccessToken,再交给 AtManager.checkAccessTokenSync() 查询:

private tokenId(): number {
  const info = bundleManager.getBundleInfoForSelfSync(0);
  return info.appInfo.accessTokenId;
}

private async checkPermission(value: number): Promise<number> {
  const name = this.permissionName(value);
  if (name === null || this.context === null) {
    return 0;
  }

  const status = abilityAccessCtrl.createAtManager().checkAccessTokenSync(
    this.tokenId(),
    name as Permissions,
  );
  return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    ? 1
    : 0;
}

这里的 1 和 0 是通道层的中间值,Dart 平台接口再把它转换为 PermissionStatus.granted 和 PermissionStatus.denied。业务代码不需要知道这个中间编码。

3. 请求权限时为什么逐个请求

requestPermissions 接收上游传来的权限枚举列表,然后逐个调用系统请求接口:

for (const permission of values) {
  output[permission] = await this.requestPermission(permission);
}

逐个请求的优点是每个权限的结果独立可追踪;缺点是一次请求多个权限时可能出现多个系统弹窗。业务层如果希望一次完成授权,应该继续使用 OHOS 原生权限组能力,而不是依赖当前兼容层的串行实现。

4. 宿主权限声明与插件 HAR 权限声明不能混为一谈

插件 HAR 的 module.json5 只描述插件模块本身:

{
  "module": {
    "name": "permission_handler",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}

真正请求相机、麦克风、定位等权限时,宿主应用的 EntryAbility 模块必须声明 requestPermissions,并提供权限理由。只修改插件 HAR 的 module.json5,不会让宿主应用自动获得权限。

5. 业务层如何处理“用户拒绝”

业务不应把 request() 的返回值直接当成成功或失败布尔值。推荐根据状态分支处理:

Future<void> ensureCameraPermission() async {
  PermissionStatus status = await Permission.camera.status;

  if (!status.isGranted) {
    status = await Permission.camera.request();
  }

  if (status.isGranted) {
    await openCamera();
    return;
  }

  await showPermissionExplanation(
    '相机权限被拒绝,无法扫描二维码。你可以在系统设置中重新开启。',
  );
}

由于当前 OHOS 版本的 openAppSettings() 返回 false,不要在业务里无条件调用 openAppSettings() 并假设页面已经打开。应先判断返回值,再决定是否显示手动前往设置的提示。

6. 推荐的验证顺序

权限适配最容易在真实设备上出现和单元测试不同的结果,建议按以下顺序验证:

  1. 未安装权限声明时调用 status,确认不会伪造授权。
  2. 在宿主 module.json5 中声明权限,重新构建 HAP。
  3. 首次点击 request(),确认系统弹窗出现。
  4. 拒绝一次后再次请求,观察系统是否仍弹窗。
  5. 撤销权限后重新启动应用,确认状态回到 denied。
  6. 对相机、麦克风、定位分别测试,不要用一个权限的结果推断另一个权限。

7. 常见故障排查

现象可能原因排查方式
所有权限都是 denied宿主未声明权限或枚举未映射检查 module.json5 和 permissionName()
权限弹窗不出现权限理由、usedScene 或系统策略配置错误检查宿主配置和模拟器系统设置
tokenId() 异常Bundle 信息不可用或调用时机过早在 Ability 已绑定后再请求
openAppSettings() 永远 false当前 OHOS 实现未提供稳定跳转能力业务显示手动设置提示
serviceStatus 结果不符合预期OHOS 没有等价服务开关以 notApplicable 语义处理
请求多个权限只看到一个结果权限组或系统弹窗合并逐个验证并记录 authResults

九、总结

权限适配的核心是“语义一致”,而不是“方法返回成功”:

  • OHOS 权限名由 Dart 枚举显式映射。
  • 宿主 module.json5 负责声明权限。
  • 未映射权限返回 denied,不伪造授权。
  • 设置页和 rationale 能力保持明确限制。

欢迎加入 CPF-Flutter OpenHarmony 社区,交流 Flutter 三方库适配、权限治理和虚拟机验证经验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐