前言
欢迎加入开源鸿蒙跨平台社区: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/├──
不是这里!(插件的module.json5)
└──
是这里!(宿主App的module.json5)
为什么?因为权限是App级别的概念,不是库级别的。
插件作为har包被集成到App中,权限声明必须在App的入口模块中。
2.2module.json5
权限配置
在宿主App的module.json5中添加:
{"module":
"ohos.permission.MICROPHONE",
"reason":
"$string:microphone_reason",
"usedScene":
各字段详解
字段 值 说明 是否必填 name “ohos.permission.MICROPHONE” 权限标识符 ✅必填
reason “$string:microphone_reason” 申请原因(展示给用户) ✅必填
usedScene.abilities [“EntryAbility”] 使用权限的Ability ✅必填
usedScene.when “inuse” 使用时机 ✅必填
2.4
reason字符串资源
reason字段引用的是字符串资源,需要在resources/base/element/string.json中定义:
{"string":[{"name":"microphone_reason","value":"用于语音识别功能,将您的语音转换为文字"}]}
📌reason的重要性:这个字符串会显示在权限弹窗中,告诉用户为什么需要这个权限。
写得好不好直接影响用户的授权意愿。
建议用简洁明了的语言说明用途,避免"需要麦克风权限"这种废话式描述。
2.5
when字段的取值
值 含义 适用场景 “inuse” 使用时申请 大多数场景(推荐) “always” 始终需要 后台持续使用的场景
flutter_speech用"inuse"就够了,因为语音识别只在用户主动操作时才需要麦克风。
三、module.json5requestPermissions
多权限声明
如果你的插件需要多个权限,可以在requestPermissions数组中添加多项:
"requestPermissions":"name":
"ohos.permission.MICROPHONE",
"reason":
"$string:microphone_reason",
"usedScene":
"ohos.permission.INTERNET",
"reason":
"$string:internet_reason",
"usedScene":
]
flutter_speech只需要MICROPHONE一个权限。
但如果你的应用还需要网络权限(在线识别需要网络),可以一并声明。
💡小技巧:ohos.permission.INTERNET是system_grant类型,不需要动态申请,声明即可使用。
但ohos.permission.MICROPHONE是user_grant类型,必须动态申请。
3.2
权限声明的验证
怎么确认权限声明是否正确?
#方法1:查看编译后的module.json
#在DevEco
然后在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:
import{abilityAccessCtrl}from'@kit.AbilityKit';
核心API:
方法 功能 返回类型 createAtManager() 创建权限管理器 AtManager atManager.requestPermissionsFromUser(context,permissions)
动态申请权限 Promise<PermissionRequestResult> atManager.checkAccessTokenSync(tokenId,permission)
检查权限状态 GrantStatus
4.2
flutter_speech中的权限申请代码
这是flutter_speechactivate方法中的权限申请部分,逐行解析:
privateasyncactivate(locale:string,result:MethodResult):Promise<void>{try{console.info(TAG,`activatecalled
locale:
${locale}`);//检查abilityContext是否可用
if(this.abilityContext){console.info(TAG,`requestingmicrophone
permission...
`);//创建权限管理器
constatManager=abilityAccessCtrl.createAtManager();//发起权限申请(会弹出系统弹窗)
constgrantResult=awaitatManager.requestPermissionsFromUser(this.abilityContext,//UIAbilityContext
['ohos.permission.MICROPHONE']//权限列表
);//检查所有权限是否都被授予
constallGranted=grantResult.authResults.every((status:number)=>status===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);console.info(TAG,`permissiongranted:
${allGranted}`);//权限被拒绝的处理
if(!allGranted){result.error('SPEECH_PERMISSION_DENIED','Microphonepermission
denied'
,null);return;}}else{//Context不可用的处理
console.error(TAG,`abilityContextnull
`);result.error('SPEECH_CONTEXT_ERROR','UIAbilityContextnot
available'
,null);return;}//权限获取成功,继续后续流程...
}catch(e){console.error(TAG,`activateerror:
${JSON.stringify(e)}`);result.error('SPEECH_ACTIVATION_ERROR',`Failedactivate:
${JSON.stringify(e)}`,null);}}
4.3
代码流程图
activate(locale,result)
result.error('SPEECH_CONTEXT_ERROR')
return
直接返回PERMISSION_GRANTED(不弹窗)
├──
result.error('SPEECH_PERMISSION_DENIED')
return
权限获取成功,继续创建引擎...
4.4
返回值解析
interfacePermissionRequestResult{permissions:Array<string>;//申请的权限列表
authResults:Array<number>;//每个权限的授权结果
}
authResults中每个元素的含义:
值 常量 含义 0 PERMISSION_GRANTED 已授权 -1 PERMISSION_DENIED 已拒绝
//检查结果的正确方式
constallGranted=grantResult.authResults.every((status:number)=>status===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);//也可以用索引检查单个权限
constmicGranted=grantResult.authResults[0]===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
4.5
权限状态检查(不弹窗)
有时候你只想检查权限状态,不想弹窗。
可以用checkAccessTokenSync:
privatecheckPermissionStatus():boolean{if(!this.abilityContext)returnfalse;constatManager=abilityAccessCtrl.createAtManager();consttokenId=this.abilityContext.applicationInfo.accessTokenId;conststatus=atManager.checkAccessTokenSync(tokenId,'ohos.permission.MICROPHONE');returnstatus===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;}
这个方法是同步的,不会弹窗,只返回当前权限状态。
适合在UI中显示权限状态,或者在非关键路径上做权限预检查。
💡使用场景:比如在App启动时检查权限状态,如果已授权就直接显示"开始识别"按钮;如果未授权就显示"需要麦克风权限"的提示。
五、权限拒绝的处理策略与用户提示
5.1
用户拒绝权限的场景
用户可能在以下场景拒绝权限:
场景 表现 后续行为 首次弹窗点击"拒绝" authResults返回-1 下次还会弹窗 勾选"不再询问"后拒绝 authResults返回-1 不再弹窗,需要引导去设置 在系统设置中关闭 checkAccessToken返回-1 需要引导去设置
5.2
flutter_speech的当前处理
if(!allGranted){result.error('SPEECH_PERMISSION_DENIED','Microphonepermission
denied'
,null);return;}
当前的处理比较简单——直接返回错误。
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不会再弹窗。
这时需要引导用户手动去系统设置中开启权限:
//OpenHarmony跳转到App设置页
import{Want}from'@kit.AbilityKit';privateopenAppSettings():void{if(!this.abilityContext)return;constwant: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:
//检查权限
if(ContextCompat.checkSelfPermission(activity,Manifest.permission.RECORD_AUDIO)!=PackageManager.PERMISSION_GRANTED){//申请权限
ActivityCompat.requestPermissions(activity,newString[]{Manifest.permission.RECORD_AUDIO},REQUEST_CODE_RECORD_AUDIO);}//结果回调(在另一个方法中)
@OverridepublicvoidonRequestPermissionsResult(intrequestCode,String[]permissions,int[]grantResults){if(requestCode==REQUEST_CODE_RECORD_AUDIO){if(grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){//权限已授予
}}}
iOS:
//申请语音识别权限
[SFSpeechRecognizerrequestAuthorization:^(SFSpeechRecognizerAuthorizationStatus
status){if(status==SFSpeechRecognizerAuthorizationStatusAuthorized){//
再申请麦克风权限[[AVAudioSession
sharedInstance]requestRecordPermission:^(BOOL
granted){if(granted){//
两个权限都获得了}}];}}];
OpenHarmony:
//一步搞定
constatManager=abilityAccessCtrl.createAtManager();constgrantResult=awaitatManager.requestPermissionsFromUser(this.abilityContext,['ohos.permission.MICROPHONE']);constallGranted=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在权限申请的关键节点都加了日志:
console.info(TAG,`requestingmicrophone
permission...
`);//...
申请权限
console.info(TAG,`permissiongranted:
${allGranted}`);
查看日志:
hdchilog|grep"FlutterSpeechPlugin"|grep-i"permission"
7.2
权限状态重置
测试时经常需要重置权限状态:
#uninstall
com.example.flutter_speech_example#
重新安装
#方法2:在系统设置中手动关闭权限
#应用管理
开发阶段可以临时强制模拟权限拒绝
privateasyncactivate(locale:string,result:MethodResult):Promise<void>{//调试用:模拟权限拒绝
//const
result.error('SPEECH_PERMISSION_DENIED',
'Debug:
正常流程...
}
7.4
常见问题排查
问题 可能原因 排查方法 弹窗不出现 module.json5未声明权限 检查宿主App的module.json5 弹窗不出现 用户已勾选"不再询问" 卸载重装或去设置页开启 直接返回denied 权限名拼写错误 检查权限字符串 申请崩溃 abilityContext为null 确认onAttachedToAbility已调用 授权后仍无法使用 权限声明位置错误 确认声明在宿主App中
八、权限申请的完整检查清单
type="checkbox"
/>宿主App的module.json5中声明了ohos.permission.MICROPHONE
type="checkbox"
/>reason字段引用了有效的字符串资源
type="checkbox"
/>usedScene配置了正确的Ability名称
type="checkbox"
/>代码中使用abilityAccessCtrl.createAtManager()创建管理器
type="checkbox"
/>使用requestPermissionsFromUser动态申请权限
type="checkbox"
/>正确检查authResults中的授权状态
type="checkbox"
/>处理了权限拒绝的情况(返回错误码)
type="checkbox"
/>处理了abilityContext为null的情况
type="checkbox"
/>在真机上测试了权限弹窗流程
总结
本文详细讲解了flutter_speech中麦克风权限的完整实现:
- 权限模型:OpenHarmony采用"声明+动态申请"两步走模式
- 声明位置:权限声明在宿主App的module.json5中,不是插件的
- 动态申请:使用
abilityAccessCtrl.requestPermissionsFromUser,支持async/await - 结果处理:检查
authResults数组中每个权限的授权状态 - 拒绝处理:返回错误码,Dart层做友好提示和设置页引导
下一篇我们讲语音识别引擎的创建——speechRecognizer.createEngine的参数详解和异常处理。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
/>
相关资源:
- OpenHarmony权限管理文档
- abilityAccessCtrl
API参考
- module.json5配置说明
- Android权限申请指南
- iOS权限申请指南
- flutter_speech
OpenHarmony源码
- 开源鸿蒙跨平台社区
- OpenHarmony权限列表


