ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony仓颉实战:用Want实现一键拨号与权限避坑指南

OpenHarmony仓颉实战:用Want实现一键拨号与权限避坑指南 1. 为什么一键拨号值得在 OpenHarmony 上花一整篇文章1.1 一个看似简单却到处是门道的功能做 OpenHarmony 应用的人应该都有这种体会系统 API 看着挺全真跑起来处处是门槛。拨打电话就是非常典型的一个。表面上是几行代码的事实际上牵扯到你开发的 App 以什么身份运行普通应用、系统应用、特权应用你选择唤起系统拨号盘还是直接外呼你使用的 SDK 版本支持哪个动作字符串你的应用有没有配置正确的签名和权限等级这里要先把两件事分开讲清楚因为它们对应完全不同的实现代码和验收路径。第一件事是你的应用只是跳转到系统自带的拨号界面把用户输入的号码带过去用户看到拨号盘后自己点绿色按键。这段链路不需要任何敏感权限普通应用签名就能跑通。第二件事是你的应用直接通过电话子系统发起呼叫用户点了按钮之后系统直接开始拨号没有中间确认环节。这条链路需要PLACE_CALL权限。而PLACE_CALL在 OpenHarmony 权限体系里属于system_core级别默认只有系统应用、或者经过特殊签名授权的应用才能拿到。所以动手写代码之前第一件事不是查 API而是确认自己的应用身份和业务需求。我见过不少同事一上来就申请权限结果签名校验不过白折腾一下午。1.2 仓颉语言在这个场景下的特殊性仓颉Cangjie作为面向 OpenHarmony/HarmonyOS 生态的新型编程语言目前在开发者社区的实战资料还比较少。大家熟悉的 ArkTS Stage 模型教程很多但换成仓颉语法之后很多东西要重新对一遍组件声明方式不同状态管理装饰器不同异步处理方式不同依赖导入路径不同最直接的影响是你在网上搜OpenHarmony 拨打电话出来的示例代码大多是 ArkTS 的直接抄过来在仓颉工程里根本没法编译。这篇文章里的代码示例我都按仓颉的语法风格来写同时标明哪些是 SDK 版本相关、需要你自己替换的部分这样至少能让你少走一半弯路。1.3 我最终选择的技术路线如果只是做一个给普通用户使用的工具类应用我的建议非常明确走跳转拨号盘这条路即通过Want携带tel:URI 唤起系统的拨号 Ability。对用户多一步确认反而更安全符合日常使用习惯对开发者避开权限申请的坑普通应用签名即可发布对维护者不依赖系统定制OpenHarmony 各发行版都能跑后面的内容我会先讲原理再给完整代码最后把真机上遇到的坑列出来。如果你确实需要直接外呼我也在第 5 章单独说明权限申请和签名配置的方法但那是另一个量级的工作量。2. 先把环境搞定DevEco Studio 里的仓颉工程2.1 版本选择与 SDK 安装仓颉语言目前在官方 IDE 里的支持是通过插件形式提供的。我这次使用的是 DevEco Studio 5.0 版本配合仓颉 SDK 插件。这里有个细节值得注意仓颉 SDK 并不是随 IDE 默认装好的你要到工具链管理里手动添加。添加之后新建工程时才会出现 Cangjie Application 这个模板选项。另外需要提醒的是仓颉 SDK 有独立于 OpenHarmony SDK 的版本号。我的经验是尽量让仓颉 SDK 的版本和 OpenHarmony SDK 的主版本保持一致。比如 OpenHarmony API 版本是 12 的话仓颉 SDK 也应该选择对应支持 12 的版本否则编译时会出现系统 API 找不到的情况而且这种错误通常在运行时才暴露排查成本很高。如果你需要用到stdx这类扩展库也要先确认它是否由当前 SDK 直接提供没有的话需要单独引入别等到编译报错才去翻文档。2.2 创建工程前要先想清楚的几件事新建仓颉工程时需要填写 bundleName、versionName、versionCode 这些基本信息。有几个字段我想多说两句因为这些在后续真机调试时会直接影响效率BundleName用反域名格式比如com.example.phonecall注意整段不能以数字开头否则签名工具会直接拒绝。versionCode每次上真机调试建议递增否则安装到已经存在旧包名的设备上时偶尔会出现安装失败版本号冲突的情况。签名配置默认的自动签名只适用于调试。如果要测试PLACE_CALL权限必须用系统级签名证书这个我在第 5 章展开。创建完成后工程目录大概是这样的结构PhoneCallDemo/ ├── App.cj // 应用入口 ├── module.json5 // 模块配置权限声明在这里 ├── src/ │ └── main/ │ ├── cangjie/ // 仓颉源码目录 │ └── resources/ // 资源和配置文件 └── oh-package.json5 // 依赖声明我建议把页面代码放在cangjie目录下单独的pages子目录里方便后续扩展。2.3 工程里最容易出问题的一个配置提前说一个后续一定会遇到的坑module.json5中的requestPermissions字段。如果走跳转拨号盘方案其实不需要声明任何权限但很多人习惯性地把电话权限写进去结果编译报错——因为普通应用声明了PLACE_CALL这种system_core级权限在安装或编译阶段就会被拒绝。如果你确实需要声明格式长这样{ module: { requestPermissions: [ { name: ohos.permission.PLACE_CALL, reason: $string:reason_place_call, usedScene: { abilities: [EntryAbility] } } ] } }这里有一个容易忽略的点reason字段必须引用字符串资源不能直接写一段中文。很多新手在这里卡住编译一直报reason 不是有效的资源引用。解决办法是在resources目录的 string.json 里加一条对应的字符串。3. 拨号功能的底层机制Want、Ability 与权限等级3.1 Want 到底是干什么的想要理解拨打电话这个需求绕不开Want这个概念。你可以把Want想象成一个带地址的信封应用 A 要把某个动作交给系统或者其他应用去处理就把动作信息和数据装进这个信封里然后交给系统分发。信封上常见的信息包括字段作用拨号场景的值action描述要做什么ohos.want.action.dialuri描述操作对象tel:138xxxxbundleName指定目标应用留空表示由系统解析abilityName指定目标页面留空表示由系统选择parameters附带参数可传扩展字段在拨打电话这个场景里我们通常只填action和uri就够了。系统看到 action 是dialuri 是tel:开头的字符串就会自动找到能够处理拨号意图的 Ability 并唤起它。这种由系统解析目标的方式在 OpenHarmony 里叫隐式Want好处是你不用关心系统里到底是哪个应用来处理拨号也不用在代码里写死包名和应用名。3.2 为什么必须用 tel: 前缀如果你在浏览器里输入tel:10086手机也会弹出拨号界面。这里也是同一个机制——URI 的 scheme 决定了这个意图是给谁处理的。在 OpenHarmony 的隐式Want匹配规则里系统会根据 uri 的 scheme也就是冒号前面的部分来做分发。tel:就是电话的 scheme。所以拼接 uri 的时候要格外小心正确写法tel:13800138000错误写法直接在 uri 里放一串没有前缀的数字很多入门资料里会把 uri 写成tel: number这种字符串拼接我建议你用语言内置的字符串插值比如tel:${phoneNumber}少一层手动拼接就少一个出错的机会。3.3 system_core 权限那个绕不开的门槛OpenHarmony 的权限体系大致分三个等级normal普通权限应用声明后即可获得比如读取网络状态。system_basic基础系统权限需要系统授信签名但一般第三方应用经过审核也能拿比如读取位置信息。system_core核心系统权限只授予系统应用用于对系统敏感资源的操作。PLACE_CALL属于system_core。这就意味着普通应用即使声明了它也无法通过系统校验安装时可能就会失败。除非你开发的是运营商定制应用、系统预置应用或者企业内部特殊签名的应用否则直接外呼这条路基本走不通。这其实也解释了为什么我更推荐跳转拨号盘方案——不是技术上有难度而是权限门槛对普通开发者不友好。从产品角度看多让用户点一下确认键某种程度上也是替用户把关避免误触导致不必要的通话。4. 核心代码实战从输入框到拨号盘4.1 页面 UI 怎么搭先写一个最简单的拨号页面一个文本输入框、一个按钮外加一个回显状态。UI 这部分用仓颉的声明式 UI 来写组件名字和 ArkUI 保持同构有 ArkUI 基础的话基本能无缝上手。以下是基于我当前使用的仓颉 SDK 版本写的不同版本 API 名称可能有差异但整体调用链是一致的。import ohos.arkui.component.* import ohos.arkui.layout.* import ohos.arkui.dialog.* Entry Component class PhoneDialPage { State private var phoneNumber: String State private var callStatus: String 未拨打 func build() { Column() { Text(一键呼叫) .fontSize(24) .fontWeight(FontWeight.Bold) .margin({bottom: 24}) TextInput() .placeholder(请输入电话号码) .maxLength(20) .type(InputType.PhoneNumber) .onChange { value phoneNumber value } Button(拨打) .width(100%) .margin({top: 24, bottom: 16}) .onClick { _ if (phoneNumber.isEmpty()) { callStatus 号码不能为空 } else { dialToCaller(phoneNumber) } } Text(callStatus) .fontSize(14) .fontColor(Color.Gray) } .width(100%) .height(100%) .padding(24) } }这里有两个小细节TextInput 的 type 设置成PhoneNumber后系统键盘会自动带上数字和拨号符号对用户体验很有帮助。状态变量用State修饰赋值之后 UI 会自动刷新不需要手动调用刷新方法。如果你的开发经验是命令式 UI可能会觉得这套写法有点不习惯但熟悉之后就还好。声明式 UI 最大的好处是状态和界面同步不用自己操心组件树的更新时机。4.2 拨号跳转的核心封装接下来是重点——把号码交给系统拨号盘。我把它封装成一个独立的函数方便以后在多个页面复用import ohos.base.* import ohos.ability.context import ohos.ability.want func dialToCaller(number: String) { let trimmed number.trim() if (trimmed.isEmpty()) { return } let want Want( action ohos.want.action.dial, uri tel:${trimmed} ) let context getContext(this) context.startAbility(want).then { result console.info([CALL] dial page launched) }.catch { err console.error([CALL] launch failed: ${err.message}) } }这段代码里最核心的就是Want的构造。action 用ohos.want.action.dialuri 用tel:加号码的组合。startAbility是异步的成功或失败都会走对应的回调分支。注意getContext(this)是我当前 SDK 里的写法不同版本可能不一样以你工程里可用的 API 为准。如果是在仓颉的异步模型里也可以把 then/catch 换成 await/异常捕获的写法逻辑是一样的。4.3 startAbility 的返回码意味着什么我在调试时经常会看到startAbility抛出一个 Result 对象里面带 code 和 message。先列几个常见的code含义常见原因0成功无401权限被拒系统无法处理该 uri 或权限等级不够16000004组件不存在action 拼写错误或系统没有匹配的 Ability16000006跨应用权限校验失败bundleName 指定了但签名不匹配遇到16000004时先检查 action 字符串是否写成了dial而不是ohos.want.action.dial。完整格式不带的话系统不知道你想干嘛只能返回组件不存在。4.4 用参数传递更多信息进阶如果后面你需要在拨号前自动带上通话记录备注、或者要传给特定拨号应用额外的字段可以在Want的parameters里加数据let want Want( action ohos.want.action.dial, uri tel:${number}, parameters [ callDisplayName: 客服热线, callType: service ] )不过说实话这个用法依赖接收方的实现普通系统拨号盘可能只会用到 uri 里的号码。我的建议是先用最简版本跑通确有需求再去查目标拨号应用支持的参数。5. 权限、签名与真机验证一条龙5.1 跳转方案需要做的配置清单如果全程采用跳转拨号盘方案需要准备的东西非常少代码里按第 4 章的Want构造拨号意图。不需要在module.json5里声明任何涉及电话的权限。使用 DevEco Studio 的默认自动签名即可。这其实是整个流程里我觉得最舒服的一段不用求证书、不用改系统配置。但要注意即使不走权限你的应用依然需要正常签名没有签名的包在真机上是装不上去的。5.2 直接外呼方案PLACE_CALL 的申请与签名如果你确实需要应用直接发起呼叫比如儿童手表的一键求助那么要按下面的链路来做在module.json5中声明PLACE_CALL权限并配置reason和usedScene。拿到系统的特权应用签名证书在 IDE 的签名配置里替换默认证书。如果你做的是系统预置应用还需要确认目标设备的 SELinux 策略是否放行电话子系统调用。这里多说一句即便你拿到了签名PLACE_CALL的生效还受设备是否开启仅系统应用限制的影响。部分发行版会在权限管理服务里做额外校验导致签了名也依然报错。这类问题排查起来很费时间技术上都绕不开编译、安装、运行时校验三个环节的协同哪个环节证书不匹配就直接失败。所以普通项目真的没必要碰这条链路。5.3 真机验证的完整步骤模拟器上其实不方便验证拨号建议直接用真机。步骤整理成清单打开手机设置进入开发者模式开启 USB 调试。连接电脑后在 DevEco Studio 里等设备识别出来确认包名和签名配置无误。点击 Run等待安装完成。在页面里输入一个任意号码比如 10086点击拨打。预期结果系统弹出拨号盘号码已经填好手动点拨号键即可拨出。如果点击按钮后没有任何反应先看日志过滤[CALL]或CangjieLog相关的标签常见报错就是我在第 4 章列的那几个 code。日志查看方式DevEco Studio 的 Log 面板里选择对应设备然后在过滤框里输入Cangjie或者你的包名。仓颉侧console.info的输出会带Cangjie标签比在 ArkTS 里看日志更容易分辨。5.4 从拨号盘返回之后怎么知道用户是否真的拨出去了这里补充一个很多人忽略的点跳转拨号盘之后你的 App 其实退到后台了等用户挂断电话回到 App你需要知道这次交互的结果吗在 Stage 模型里startAbility的返回值只能告诉你拨号盘是否成功唤起无法告诉你用户是否真的完成了呼叫。想要拿到完整结果一般有两种思路简化思路不关心结果。大部分工具类 App 就是这样唤起即成功用户挂断回来后保持原页面即可。进阶思路如果业务需要统计本次呼叫是否完成可以在页面的onPageShow之类的生命周期回调里做一次状态刷新或者配合后台任务做时长记录。但这类统计本质上依赖系统通话记录权限READ_CALL_LOG属于system_basic级普通应用往往也拿不到。我的建议是不要在普通应用里强求呼叫是否完成这个数据除非你确认设备支持对应权限。6. 真机踩坑记录五个让我折腾半天的实际教训6.1 action 简写导致的组件不存在第一次跑通代码时我在 action 里写的是dial想着是不是只要表达拨号这个意思就行。结果真机上按钮点了没反应日志里报了16000004。后来完整打日志才发现系统根本没有匹配到任何可以处理dial动作的组件。改成ohos.want.action.dial之后一切正常。这个坑让我彻底明白了OpenHarmony 的系统 action 全都是有对应常量或完整字符串的不能按照直觉简写。6.2 Previewer 和模拟器上的没反应不是代码问题我在开发过程中用预览器测试点击按钮始终没有效果一度怀疑是Want传参出问题。后来把代码跑到真机上一次就成功了。原因很简单Previewer 只渲染 UI不会真的启动系统电话能力。模拟器的情况类似很多模拟器镜像没有集成完整的电话子系统或者拨号盘应用根本没装。所以遇到点击无反应先确认你是在真机上验证的。6.3 号码中的特殊字符导致 uri 解析失败有一次测试国际号码时我输入了86 138 0013 8000结果拨号盘打开的瞬间号码被截断了。排查后发现是空格和号在 URI 里属于特殊字符系统解析时按 URI 语法把空格当作分隔符处理了。解决办法是传给 uri 前先编码或者干脆在输入阶段就做清洗把空格、横杠全部去掉只保留数字和必要的。我现在习惯在dialToCaller函数里统一做一步清洗避免不同页面重复踩坑func normalizePhoneNumber(input: String): String { var result StringBuilder() for (c in input) { if (c.isDigit() || c || c # || c *) { result.append(c) } } return result.toString() }6.4 返回码 401 的真相权限等级不匹配有一次我在一个签名测试的工程里把跳转改成了直接外呼的调用方式结果startAbility直接返回 401。后来查文档才反应过来401 代表的是权限等级不匹配——你的应用身份是普通应用却试图调用系统核心能力。想通之后直接把代码改回跳转方案问题立刻消失。如果你也遇到 401先别急着翻日志第一时间确认自己是不是触碰了system_core权限。6.5 仓颉调试的技巧日志过滤与断点结合仓颉工程的调试体验和 ArkTS 略有差别。我常用的做法是代码里关键位置打上console.info([CALL] action action , uri uri)然后在 DevEco Studio 的 Log 面板过滤[CALL]。注意仓颉控制台输出的中文在部分终端模拟器里会乱码建议核心日志同时带一个纯英文标签这样过滤起来最稳。断点调试方面仓颉在 IDE 里支持源码断点但要注意断点打在 lambda 表达式内部时步进行为有时候会比较怪看起来像跳到了源码之外。我的建议是把关键逻辑先抽成独立函数再在函数入口打断点调试体验和排查效率都会高很多。最后分享一点个人体会。把拨打电话这个功能真正落地之后我发现它最大的价值不是那几行代码而是逼着我把 OpenHarmony 的应用身份、权限体系、Want分发机制这几个概念串了起来。尤其是权限等级它跟文档里看到的权限列表完全是两回事。如果你也在用仓颉做 OpenHarmony 开发建议从小功能开始把设备上电、安装、运行、日志这条链路理顺之后再考虑直接外呼这类高权限能力效率会高很多。另一个小技巧做完这个功能后可以把拨号入口封装成公共组件放到自己的基础库里后面做客服、通讯录、门禁这类业务时一个参数传进去就能复用。
返回列表