前言
欢迎加入开源鸿蒙跨平台社区: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中麦克风权限的完整实现:
- 权限模型:OpenHarmony采用"声明+动态申请"两步走模式
- 声明位置:权限声明在宿主App的module.json5中,不是插件的
- 动态申请 :使用
abilityAccessCtrl.requestPermissionsFromUser,支持async/await - 结果处理 :检查
authResults数组中每个权限的授权状态 - 拒绝处理:返回错误码,Dart层做友好提示和设置页引导
下一篇我们讲语音识别引擎的创建 ------speechRecognizer.createEngine的参数详解和异常处理。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
