Flutter三方库适配OpenHarmony【flutter_speech】— 麦克风权限申请实现

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

语音识别的第一道门槛不是技术实现,而是权限。没有麦克风权限,后面的一切都是空谈。

OpenHarmony的权限模型和Android类似,都是"声明+动态申请"的两步走模式。但具体的API和流程有不少差异。我在适配flutter_speech的时候,权限这块花了不少时间------不是因为API复杂,而是因为权限声明的位置搞错了。

插件的module.json5?宿主App的module.json5?到底在哪里声明?这个问题困扰了我好一阵。今天把这些坑都讲清楚,让大家少走弯路。

💡 核心知识点 :OpenHarmony权限模型、module.json5权限声明、abilityAccessCtrl动态申请、权限拒绝处理。

一、OpenHarmony 权限模型概述

1.1 权限分类

OpenHarmony把权限分为两大类:

类型 说明 申请方式 示例
system_grant 系统授权权限 安装时自动授予 网络访问、振动
user_grant 用户授权权限 运行时弹窗申请 麦克风、相机、位置

麦克风权限ohos.permission.MICROPHONE属于user_grant类型,必须在运行时动态申请,用户手动授权后才能使用。

1.2 权限申请的两步走

复制代码
第1步:静态声明(编译时)
  └── 在module.json5的requestPermissions中声明

第2步:动态申请(运行时)
  └── 调用abilityAccessCtrl.requestPermissionsFromUser()弹窗

两步缺一不可:

  • 只声明不申请:App不会崩溃,但权限不会生效,麦克风调用会静默失败
  • 只申请不声明requestPermissionsFromUser会直接返回拒绝,不弹窗

🤦 我的踩坑经历 :我一开始只在代码里写了动态申请,忘了在module.json5里声明。结果权限弹窗死活不出来,requestPermissionsFromUser直接返回denied。排查了半天才发现是声明缺失。

1.3 与Android权限模型的对比

对比项 Android OpenHarmony
静态声明文件 AndroidManifest.xml module.json5
权限名称 android.permission.RECORD_AUDIO ohos.permission.MICROPHONE
动态申请API ActivityCompat.requestPermissions atManager.requestPermissionsFromUser
结果获取 onRequestPermissionsResult回调 await直接获取
申请上下文 Activity UIAbilityContext
权限分组 有(危险权限组) 无分组概念

二、ohos.permission.MICROPHONE 权限声明

2.1 权限声明的位置

这是最容易搞混的地方------权限声明要放在宿主应用的module.json5中,而不是插件的module.json5中。

复制代码
flutter_speech_recognition/
├── ohos/
│   └── src/main/
│       └── module.json5          ← ❌ 不是这里!(插件的module.json5)
│
└── example/
    └── ohos/
        └── entry/
            └── src/main/
                └── module.json5  ← ✅ 是这里!(宿主App的module.json5)

为什么?因为权限是App级别的概念,不是库级别的。插件作为har包被集成到App中,权限声明必须在App的入口模块中。

2.2 module.json5 权限配置

在宿主App的module.json5中添加:

json5 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "default",
      "tablet"
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:microphone_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
    // ... 其他配置
  }
}

2.3 各字段详解

字段 说明 是否必填
name "ohos.permission.MICROPHONE" 权限标识符 ✅ 必填
reason "$string:microphone_reason" 申请原因(展示给用户) ✅ 必填
usedScene.abilities ["EntryAbility"] 使用权限的Ability ✅ 必填
usedScene.when "inuse" 使用时机 ✅ 必填

2.4 reason字符串资源

reason字段引用的是字符串资源,需要在resources/base/element/string.json中定义:

json 复制代码
{
  "string": [
    {
      "name": "microphone_reason",
      "value": "用于语音识别功能,将您的语音转换为文字"
    }
  ]
}

📌 reason的重要性:这个字符串会显示在权限弹窗中,告诉用户为什么需要这个权限。写得好不好直接影响用户的授权意愿。建议用简洁明了的语言说明用途,避免"需要麦克风权限"这种废话式描述。

2.5 when字段的取值

含义 适用场景
"inuse" 使用时申请 大多数场景(推荐)
"always" 始终需要 后台持续使用的场景

flutter_speech用"inuse"就够了,因为语音识别只在用户主动操作时才需要麦克风。

三、module.json5 中 requestPermissions 配置

3.1 多权限声明

如果你的插件需要多个权限,可以在requestPermissions数组中添加多项:

json5 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:microphone_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.INTERNET",
    "reason": "$string:internet_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "always"
    }
  }
]

flutter_speech只需要MICROPHONE一个权限。但如果你的应用还需要网络权限(在线识别需要网络),可以一并声明。

💡 小技巧ohos.permission.INTERNET是system_grant类型,不需要动态申请,声明即可使用。但ohos.permission.MICROPHONE是user_grant类型,必须动态申请。

3.2 权限声明的验证

怎么确认权限声明是否正确?

bash 复制代码
# 方法1:查看编译后的module.json
# 在DevEco Studio中,Build → Build Hap(s)/APP(s)
# 然后在build目录下找到编译后的module.json,确认requestPermissions存在

# 方法2:运行时日志验证
# 如果权限声明正确,requestPermissionsFromUser会弹出系统权限弹窗
# 如果声明缺失,会直接返回denied,不弹窗

3.3 常见声明错误

错误 症状 解决
权限名拼写错误 弹窗不出现 检查权限名是否完全正确
reason缺失 编译报错 添加reason字段和对应字符串资源
声明在插件module.json5中 弹窗不出现 移到宿主App的module.json5中
usedScene缺失 可能编译警告 添加完整的usedScene配置
abilities名称错误 权限可能不生效 确认Ability名称和实际一致

四、abilityAccessCtrl 动态权限申请实现

4.1 API介绍

abilityAccessCtrl是OpenHarmony的权限管理模块,提供了权限检查和申请的API:

typescript 复制代码
import { abilityAccessCtrl } from '@kit.AbilityKit';

核心API:

方法 功能 返回类型
createAtManager() 创建权限管理器 AtManager
atManager.requestPermissionsFromUser(context, permissions) 动态申请权限 Promise<PermissionRequestResult>
atManager.checkAccessTokenSync(tokenId, permission) 检查权限状态 GrantStatus

4.2 flutter_speech中的权限申请代码

这是flutter_speech activate方法中的权限申请部分,逐行解析:

typescript 复制代码
private async activate(locale: string, result: MethodResult): Promise<void> {
  try {
    console.info(TAG, `activate called with locale: ${locale}`);

    // 1. 检查abilityContext是否可用
    if (this.abilityContext) {
      console.info(TAG, `requesting microphone permission...`);

      // 2. 创建权限管理器
      const atManager = abilityAccessCtrl.createAtManager();

      // 3. 发起权限申请(会弹出系统弹窗)
      const grantResult = await atManager.requestPermissionsFromUser(
        this.abilityContext,                    // UIAbilityContext
        ['ohos.permission.MICROPHONE']          // 权限列表
      );

      // 4. 检查所有权限是否都被授予
      const allGranted = grantResult.authResults.every(
        (status: number) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      );

      console.info(TAG, `permission granted: ${allGranted}`);

      // 5. 权限被拒绝的处理
      if (!allGranted) {
        result.error('SPEECH_PERMISSION_DENIED',
          'Microphone permission denied', null);
        return;
      }
    } else {
      // 6. Context不可用的处理
      console.error(TAG, `abilityContext is null`);
      result.error('SPEECH_CONTEXT_ERROR',
        'UIAbilityContext not available', null);
      return;
    }

    // 权限获取成功,继续后续流程...
  } catch (e) {
    console.error(TAG, `activate error: ${JSON.stringify(e)}`);
    result.error('SPEECH_ACTIVATION_ERROR',
      `Failed to activate: ${JSON.stringify(e)}`, null);
  }
}

4.3 代码流程图

复制代码
activate(locale, result)
    │
    ├── abilityContext == null?
    │   └── 是 → result.error('SPEECH_CONTEXT_ERROR') → return
    │
    ├── 创建AtManager
    │
    ├── requestPermissionsFromUser()
    │   │
    │   ├── 首次申请 → 弹出系统权限弹窗
    │   │   ├── 用户点击"允许" → PERMISSION_GRANTED
    │   │   └── 用户点击"拒绝" → PERMISSION_DENIED
    │   │
    │   └── 已授权 → 直接返回PERMISSION_GRANTED(不弹窗)
    │
    ├── allGranted == false?
    │   └── 是 → result.error('SPEECH_PERMISSION_DENIED') → return
    │
    └── 权限获取成功,继续创建引擎...

4.4 requestPermissionsFromUser 返回值解析

typescript 复制代码
interface PermissionRequestResult {
  permissions: Array<string>;    // 申请的权限列表
  authResults: Array<number>;    // 每个权限的授权结果
}

authResults中每个元素的含义:

常量 含义
0 PERMISSION_GRANTED 已授权
-1 PERMISSION_DENIED 已拒绝
typescript 复制代码
// 检查结果的正确方式
const allGranted = grantResult.authResults.every(
  (status: number) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);

// 也可以用索引检查单个权限
const micGranted = grantResult.authResults[0] ===
  abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;

4.5 权限状态检查(不弹窗)

有时候你只想检查权限状态,不想弹窗。可以用checkAccessTokenSync

typescript 复制代码
private checkPermissionStatus(): boolean {
  if (!this.abilityContext) return false;

  const atManager = abilityAccessCtrl.createAtManager();
  const tokenId = this.abilityContext.applicationInfo.accessTokenId;

  const status = atManager.checkAccessTokenSync(
    tokenId,
    'ohos.permission.MICROPHONE'
  );

  return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}

这个方法是同步的,不会弹窗,只返回当前权限状态。适合在UI中显示权限状态,或者在非关键路径上做权限预检查。

💡 使用场景:比如在App启动时检查权限状态,如果已授权就直接显示"开始识别"按钮;如果未授权就显示"需要麦克风权限"的提示。

五、权限拒绝的处理策略与用户提示

5.1 用户拒绝权限的场景

用户可能在以下场景拒绝权限:

场景 表现 后续行为
首次弹窗点击"拒绝" authResults返回-1 下次还会弹窗
勾选"不再询问"后拒绝 authResults返回-1 不再弹窗,需要引导去设置
在系统设置中关闭 checkAccessToken返回-1 需要引导去设置

5.2 flutter_speech的当前处理

typescript 复制代码
if (!allGranted) {
  result.error('SPEECH_PERMISSION_DENIED',
    'Microphone permission denied', null);
  return;
}

当前的处理比较简单------直接返回错误。Dart层收到这个错误后,可以做更友好的提示:

dart 复制代码
_speech.activate('zh_CN').then((res) {
  setState(() => _speechRecognitionAvailable = res);
}).catchError((e) {
  if (e.toString().contains('SPEECH_PERMISSION_DENIED')) {
    // 显示友好提示
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text('需要麦克风权限'),
        content: Text('语音识别功能需要使用麦克风,请在设置中开启权限。'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: Text('取消')),
          TextButton(onPressed: () {
            // 跳转到App设置页
            Navigator.pop(ctx);
          }, child: Text('去设置')),
        ],
      ),
    );
  }
});

5.3 引导用户到设置页

如果用户勾选了"不再询问",requestPermissionsFromUser不会再弹窗。这时需要引导用户手动去系统设置中开启权限:

typescript 复制代码
// OpenHarmony跳转到App设置页
import { Want } from '@kit.AbilityKit';

private openAppSettings(): void {
  if (!this.abilityContext) return;

  const want: Want = {
    bundleName: 'com.huawei.hmos.settings',
    abilityName: 'com.huawei.hmos.settings.MainAbility',
    uri: 'application_info_entry',
    parameters: {
      pushParams: this.abilityContext.applicationInfo.name
    }
  };

  this.abilityContext.startAbility(want);
}

⚠️ 注意:跳转设置页的方式可能因系统版本不同而有差异。上面的代码是一种常见的实现方式,但不保证在所有设备上都能正常工作。

5.4 权限处理的最佳实践

复制代码
用户点击"开始识别"
    │
    ├── 检查权限状态(checkAccessTokenSync)
    │   │
    │   ├── 已授权 → 直接开始识别
    │   │
    │   └── 未授权 → 申请权限(requestPermissionsFromUser)
    │       │
    │       ├── 用户授权 → 开始识别
    │       │
    │       └── 用户拒绝
    │           │
    │           ├── 首次拒绝 → 提示"需要麦克风权限才能使用语音识别"
    │           │
    │           └── 永久拒绝 → 提示"请在设置中开启麦克风权限" + 跳转按钮
    │
    └── 结束

六、三平台权限实现对比

6.1 代码对比

Android

java 复制代码
// 检查权限
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
        != PackageManager.PERMISSION_GRANTED) {
    // 申请权限
    ActivityCompat.requestPermissions(activity,
        new String[]{Manifest.permission.RECORD_AUDIO},
        REQUEST_CODE_RECORD_AUDIO);
}

// 结果回调(在另一个方法中)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == REQUEST_CODE_RECORD_AUDIO) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限已授予
        }
    }
}

iOS

objc 复制代码
// 申请语音识别权限
[SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {
    if (status == SFSpeechRecognizerAuthorizationStatusAuthorized) {
        // 再申请麦克风权限
        [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
            if (granted) {
                // 两个权限都获得了
            }
        }];
    }
}];

OpenHarmony

typescript 复制代码
// 一步搞定
const atManager = abilityAccessCtrl.createAtManager();
const grantResult = await atManager.requestPermissionsFromUser(
  this.abilityContext,
  ['ohos.permission.MICROPHONE']
);
const allGranted = grantResult.authResults.every(
  (s: number) => s === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);

6.2 对比总结

维度 Android iOS OpenHarmony
代码行数 ~15行 ~12行 ~6行
异步模式 回调分离 嵌套回调 async/await
权限数量 1个 2个 1个
结果获取 另一个方法回调 Block闭包 直接await
代码可读性 中等 较差(嵌套)

😊 个人评价:OpenHarmony的权限申请API是三个平台中设计得最好的。async/await让代码是线性的,不需要处理回调地狱。Android的回调分离和iOS的嵌套回调都不如这种方式直观。

七、权限相关的调试技巧

7.1 日志输出

flutter_speech在权限申请的关键节点都加了日志:

typescript 复制代码
console.info(TAG, `requesting microphone permission...`);
// ... 申请权限
console.info(TAG, `permission granted: ${allGranted}`);

查看日志:

bash 复制代码
hdc hilog | grep "FlutterSpeechPlugin" | grep -i "permission"

7.2 权限状态重置

测试时经常需要重置权限状态:

bash 复制代码
# 方法1:卸载重装App
hdc uninstall com.example.flutter_speech_example
# 重新安装

# 方法2:在系统设置中手动关闭权限
# 设置 → 应用管理 → flutter_speech_example → 权限 → 麦克风 → 关闭

7.3 模拟权限拒绝

typescript 复制代码
// 开发阶段可以临时强制模拟权限拒绝
private async activate(locale: string, result: MethodResult): Promise<void> {
  // 调试用:模拟权限拒绝
  // const DEBUG_FORCE_DENY = true;
  // if (DEBUG_FORCE_DENY) {
  //   result.error('SPEECH_PERMISSION_DENIED', 'Debug: forced deny', null);
  //   return;
  // }

  // 正常流程...
}

7.4 常见问题排查

问题 可能原因 排查方法
弹窗不出现 module.json5未声明权限 检查宿主App的module.json5
弹窗不出现 用户已勾选"不再询问" 卸载重装或去设置页开启
直接返回denied 权限名拼写错误 检查权限字符串
申请崩溃 abilityContext为null 确认onAttachedToAbility已调用
授权后仍无法使用 权限声明位置错误 确认声明在宿主App中

八、权限申请的完整检查清单

  • 宿主App的module.json5中声明了ohos.permission.MICROPHONE
  • reason字段引用了有效的字符串资源
  • usedScene配置了正确的Ability名称
  • 代码中使用abilityAccessCtrl.createAtManager()创建管理器
  • 使用requestPermissionsFromUser动态申请权限
  • 正确检查authResults中的授权状态
  • 处理了权限拒绝的情况(返回错误码)
  • 处理了abilityContext为null的情况
  • 关键节点添加了日志输出
  • 在真机上测试了权限弹窗流程

总结

本文详细讲解了flutter_speech中麦克风权限的完整实现:

  1. 权限模型:OpenHarmony采用"声明+动态申请"两步走模式
  2. 声明位置:权限声明在宿主App的module.json5中,不是插件的
  3. 动态申请 :使用abilityAccessCtrl.requestPermissionsFromUser,支持async/await
  4. 结果处理 :检查authResults数组中每个权限的授权状态
  5. 拒绝处理:返回错误码,Dart层做友好提示和设置页引导

下一篇我们讲语音识别引擎的创建 ------speechRecognizer.createEngine的参数详解和异常处理。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— 窗口事件监听与应用切换检测
flutter·harmonyos
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— OpenHarmony 插件工程创建
flutter·harmonyos·鸿蒙
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— MethodChannel 通信协议设计
flutter·harmonyos
嘴贱欠吻!3 小时前
Flutter鸿蒙开发指南(十二):推荐列表数据获取
windows·flutter
嘴贱欠吻!4 小时前
Flutter鸿蒙开发指南(十三):推荐列表上拉加载
flutter
键盘鼓手苏苏4 小时前
Flutter for OpenHarmony:debounce_throttle 防抖与节流的艺术(优化用户交互与网络请求) 深度解析与鸿蒙适配指南
网络·flutter·交互
无巧不成书02184 小时前
Kotlin Multiplatform(KMP)核心解析
android·开发语言·kotlin·交互·harmonyos
Swift社区5 小时前
鸿蒙 PC 的最终形态:系统协作
华为·harmonyos
阿林来了5 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别监听器实现
人工智能·flutter·语音识别·harmonyos