
OpenHarmony源码解析之电话子系统——通话流程 原创 精华
(以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点)
王大鹏
深圳开鸿数字产业发展有限公司
一、简介
OpenAtom OpenHarmony(以下简称“OpenHarmony”)电话子系统为 OS 提供了基础的无线通信能力。
支持 TD-LTE/FDD-LTE/TD-SCDMA/WCDMA/EVDO/CDMA1X/GSM 等网络制式的通信模块,能够提供高速的无线数据传输、互联网接入等业务,具备语音、短信、彩信、SIM 卡等功能。
以下行文如无特别说明,所述说均指 OpenHarmony 系统(OpenHarmony 3.0 LTS版本)。
二、OpenHarmony架构图
三、基础知识
1. 电话子系统
电话子系统做为 OpenHarmony 很重要的组成部分,为系统提供基础的通信功能,包括 CS 域的服务,比如语音呼叫、短信、呼叫管理;也包括 PS 域的相关服务,比如 MMS、数据业务等,另外 SIM 和 RIL 的业务也在该子系统内。
2. 电话子系统架构图
OpenHarmony 现有电话子系统蜂窝通话通话相关框架图:
应用层 :各种需要通话、SMS、数据业务、SIM 卡功能的应用,例如call 应用、SMS 应用、launcher 应用等等。
框架层 :
①SDK :给应用提供标准接口,包括 JS 接口和 C++ 接口。
②Framework:向应用层提供对应模块稳定的基础能力,包括 network、call、sms、sim相关的功能,包括通话管理,短彩编辑以及发送接收,sim 卡的识别驻网,数据业务的管理等。在目前的 OpenHarmony 版本中,call_manager、cellular_call、cellular_data、data_storage、sms_mms、state_registry、core_service 等模块都属于框架层。
Hril层 :相当于安卓的 RILJ,由于不同方案使用的 Modem 不一样,所以各种指令格式,初始化序列都不一样,为了消除这些差别,而 Hril 则提供了无线硬件设备与电话服务之间的抽象层。
Vendor lib层 :类似于安卓的 RILD,负责与 modem 模块交互,发送各模块对应的 AT 命令。
Modem层:现在的基带处理器,主要处理数字信号、语音信号的编码解码以及通信协议,而基带处理器、射频和其它外围芯片作为一个 Modem 模块,提供 AT 命令接口给上层用于交互。通信模块 Modem 通过与通信网络进行沟通、传输语音及数据、完成呼叫、短信等相关电话功能。
3. 电话子系统代码结构
由于电话子系统包含较多模块,所以将各模块分开描述:
通话管理模块:主要管理 CS(Circuit Switch,电路交换)、IMS(IP Multimedia Subsystem,IP 多媒体子系统)和 OTT(over the top,OTT 解决方案)三种类型的通话,负责申请通话所需要的音视频资源,并处理多路通话时产生的各种冲突。
蜂窝通话模块:支持基于运营商网络的基础通话实现,包含基于 2G/3G 的 CS(Circuit Switch,电路交换)通话和基于 4G/5G 的 IMS(IP Multimedia Subsystem,IP 多媒体子系统)通话,包含 VoLTE/ VoWIFI/ VoNR 语音、视频、会议,支持 CS 和 IMS 通话之间的域选控制和切换,支持紧急通话。支持主流 modem 芯片平台。
蜂窝数据模块:作为电话子系统可裁剪部件,依赖于 core_service 核心服务、ril_adapter。具有蜂窝数据激活、蜂窝数据异常检测与恢复、蜂窝数据状态管理、蜂窝数据开关管理、蜂窝数据漫游管理、APN 管理、网络管理交互等功能。
电话核心服务模块:主要功能是初始化 RIL 管理、SIM 卡和搜网模块,以及获取 RIL Adapter 服务。通过注册回调服务,实现与 RIL Adapter 进行通信;通过发布订阅,来实现与各功能模块的通信。
数据库及持久化模块:负责电话服务子系统中的 SIM 卡/短彩信等模块持久化数据存储,提供 DataAbility 访问接口。
RIL Adapter模块:主要包括厂商库加载,业务接口实现以及事件调度管理。主要用于屏蔽不同 modem 厂商硬件差异,为上层提供统一的接口,通过注册 HDF 服务与上层接口通讯。
短彩信模块:为移动数据用户提供短信收发和彩信编解码功能,主要功能有 GSM/CDMA 短信收发、短信 PDU(Protocol data unit,协议数据单元)编解码、Wap Push 接收处理 、小区广播接收、彩信通知、 彩信编解码和 SIM 卡短信记录增删改查等。
状态注册模块:主要负责提供电话服务子系统各种消息事件的订阅以及取消订阅的 API。事件类型包括网络状态变化、信号强度变化、小区信息变化、蜂窝数据连接状态变化、通话状态变化等等。
4. 相关仓
核心服务 :https://gitee.com/openharmony/telephony_core_service
蜂窝通话 :https://gitee.com/openharmony/telephony_cellular_call
通话管理 :https://gitee.com/openharmony/telephony_call_manager
注册服务 :https://gitee.com/openharmony/telephony_state_registry
短彩信 :https://gitee.com/openharmony/telephony_sms_mms
riladapter:https://gitee.com/openharmony/telephony_ril_adapter
数据业务 :https://gitee.com/openharmony/telephony_cellular_data
数据存储 :https://gitee.com/openharmony/telephony_data_storage
网络管理 :https://gitee.com/openharmony/communication_netmanager_standard
5. 电话子系统(call)核心类
四、源码解析
作为电话子系统的核心业务,通话功能(Call)除了需要硬件支持,比如音频模块、基带模块等,还需要系统本身的许多服务相互配合才能实现该功能,比如:通话管理(call_manager)、蜂窝通话服务(cellular_call)、Telephony 核心服务(core_service)、RIL 适配(ril_adapter),状态注册服务(state_registry)等。
通话目前主要分成三种:CS Call(Circuit Switch,电路交换)、IMS Call(IP Multimedia Subsystem,IP 多媒体子系统)和 OTT Call(over the top,OTT 解决方案)三种类型的通话。对上层 Call 应用暴露的接口包括:dial、answer、reject、hangup、holdCall、unHoldCall、switchCal、startDTMF、stopDTMF 等。由于事件较多,而且各事件处理流程比较类似,所以我们此处以 Call 的 Answer 事件处理流程来阐述。
1. 通话上层调用代码分析
当有电话呼入时,Callui 界面会显示电话呼入,用户点击 answer 按键,则会激活 Call 的 Answer 流程。
此处会调用 incomingCom.js 的 onAnswer 函数。
由于之前已经 import了callServiceProxy.js 文件。
所以我们来看下 callServiceProxy 的 acceptCall 函数。
从代码中我们能看到此函数实际调用了 call 的 answer 函数,那么这个哪来的呢。call 是电话子系统框架层通过 napi 对外封装的接口实现的,call_manager 中的 @ohos.telephony.call.d.ts 中有对应的接口说明,从这里代码已经完成了从 app 到 framework 的调用。
2. 通话框架层代码分析
3.2.1通话框架层代码调用时序图
3.2.2通话框架层代码分析
从上边的时序图可以看出,整个 Answer 的调用流程是比较长的,整个框架层处理跨越包括 call_manager、cellular_call、core_service、IPC、ril_adapter 等多个服务。由于处理流程过长,具体的调用情况可以参照时序图,此处我们将框架层的处理一些关键地方根据调用不同的服务来进行描述。
3.2.2.1Answer事件在call_manager中的处理
之前应用层调用的 call.answer 是通过 @ohos.telephony.call 引入的,而实际定义在 call_manage 服务中的 interfaces 内的 napi 接口中实现。
注册要用到接口以及一些枚举参数napi_valueNapiCallManager::RegisterCallManagerFunc(napi_env env, napi_value exports)
这里我们需要的是 CallBasis 接口,具体内容如下所述,这些就是之前应用层调用的一些事件对应的处理函数。
因为我们对应的是 answer 事件,所以这里的对应函数是 AnswerCall。
继续往下调用,从之前的函数看 AnswerCall 应该是异步处理的。
在一路调用的的过程中,会调用 CallPolicy 类的 AnswerCallPolicy 函数用来判断对应 callId 的 CallObject 是否存在,如有有就判断 Call 所处的状态。
在后续调用到 CallRequestHandlerService 的 AnswerCall 进行处理时,如果有 CallRequestHandler 的 handler_存在,则 make 个 AnswerCallPara 的 unique_ptr,然后将 callid 和 videostate 传入该指针。调用 SendEvent 将 HANDLER_ANSWER_CALL_REQUEST 发出。
从代码的处理流程看,这里发出的 event 会被同一个文件中的 CallRequestHandler 类捕获,触发 ProcessEvent 处理。
memberFuncMap 会根据 event 的 id 从 memberFuncMap_拿到对应的 memberFunc,然后进入对应的处理函数。
这里对应的处理函数是 AcceptCallEvent(),这部分函数无返回值,又从 event 中取出 callId 和 videoState。
继续调用 CallRequestProcessl 类中的 AnswerReques 函数时,会通过 GetOneCallObject 拿到不同的 CallObject。
拿到了对应的 call 之后,就可以调用对应的 call 的 AnswerCall 函数,这个函数会覆盖 BaseCall 的虚函数。由于 CSCall、IMSCall、OTTCall 都有对应的 AnswerCall 函数,所以实际使用那个是由 BaseCall 类的虚函数 AnswerCall 被那个子类重写,从而调用不同的 AnswerCall 函数。这里我们假设为 CsCall 。
调用到 CarrierCall 的 CarrierAcceptCall 进行后续处理。其中 AcceptCallBase() 函数会判断 Call 状态,如果是 CALL_RUNNING_STATE_RINGING 状态,则会调用 AudioControlManager 的 SetVolumeAudible 来设置 audio 的相关内容。
处理完 AcceptCallBase() 后,需要用 PackCellularCallInfo 将 call 的信息打包进给 callinfo 中,后续直接该 callInfo 传递出去。
下一步会调用 CellularCallIpcInterfaceProxy 类的来 Answer 函数来继续处理。首先要判断是否有 ReConnectService 的必要,这样操作是为了保证有可用的 cellularCallInterfacePtr_。cellularCallInterfacePtr_ 是一个 IRemoteBroker 类,该类是一个 IPC 基类接口用于 IPC 通信。
在此之前 call_manager 已经在 CellularCallIpcInterfaceProxy 的初始化中将 systemAbilityId(TELEPHONY_CELLULAR_CALL_SYS_ABILITY_ID) 通过ConnectService()完成对应 SA 代理 IRemoteObject 的获取,然后构造对应的 Proxy 类,这样就能保证 IPC 通信的 proxy 和 stub 的对应。下节我们会看到,对应的 stub 其实是在 cellular_call 中注册的。
当调用到 cellularCallInterfacePtr_->Answer 时,CellularCallInterface 的 Answer 虚函数,会被 CellularCallProxy 的 Answer 重写,所以实际执行 CellularCallProxy 的 Answer 函数,它是一个 IRemoteProxy 类。而 callInfo 信息则转换成 MessageParcel 形式通过 IPC 的 SendRequest 发出。
class CellularCallProxy : public IRemoteProxy<CellularCallInterface> {
从这里开始,Answer 流程在 call_manager 的处理已经完成。
3.2.2.2 Answer事件在cellular_call中的处理
IPC(Inter-Process Communication)与RPC(Remote Procedure Call)机制用于实现跨进程通信,不同的是前者使用 Binder 驱动,用于设备内的跨进程通信,而后者使用软总线驱动,用于跨设备跨进程通信。IPC 和 RPC 通常采用客户端-服务器(Client-Server)模型,服务请求方(Client)可获取提供服务提供方(Server)的代理 (Proxy),并通过此代理读写数据来实现进程间的数据通信。
通常,Server 会先注册系统能力(System Ability)到系统能力管理者(System Ability Manager,缩写 SAMgr)中,SAMgr 负责管理这些 SA 并向 Client 提供相关的接口。Client 要和某个具体的 SA 通信,必须先从 SAMgr 中获取该 SA 的代理,然后使用代理和 SA 通信。下文使用 Proxy 表示服务请求方,Stub 表示服务提供方。实现代码在 /foundation/communication/ipc 目录下。
SA注册与启动:SA 需要将自己的 AbilityStub 实例通过 AddSystemAbility 接口注册到 SystemAbilityManager。
SA获取与调用:通过 SystemAbilityManager 的 GetSystemAbility 方法获取到对应 SA 的代理 IRemoteObject,然后构造 AbilityProxy。这样就能保证 proxy 和 stub 的对应。
上一节我们在 call_manager 中看到了 SA 的获取过程,那这里我们来看下该 SA 的注册过程,还记得 TELEPHONY_CELLULAR_CALL_SYS_ABILITY_ID这个systemAbilityId吗?它就是我们注册 SA 的关键。
这样我们就完成 stub 的注册,之前通过 proxy 发出的消息就会通过 IPC 传递到这里的 stub 中。IPC 过程比较复杂,这里就不深入讨论了,有兴趣的同学可以自学一下。
由于 IPCObjectStub 的 OnRemoteRequest 是虚函数会被继承 IPCObjectStub 类的 CellularCallStub 的 OnRemoteRequest 函数重写。
此处会根据 map 表来找到对应的处理函数,之前我们的 code = 4 是 ANSWER,此处应该会进行对应的处理。
AnswerInner 会进行后续的处理,这里会从 data 中解析出对应的 callinfo 信息,在函数末尾出调用本文件的 Answer(),并将返回的结果写入 reply 中,这个返回值是函数执行的结果,为 Int32 整形值。
Answer 函数是来完成跟ril交互的关键。首先根据 callInfo 的信息获得 calltype,目前支持三种 calltype,分别为 cs_call、ims_call、ott_call。在确定 calltype 后,通过 slotId_ 在 GetCsControl() 获得对应的 CSControl 对象。
这个调用到 CSControl::Answer 函数,会根据 callInfo 拿到对应的 CellularCallConnectionCS 和 CALL_STATUS,状态为来电、响铃、等待状态,则会调用 CellularCallConnectionCS 对应的函数。
后续会调用 CellularCallConnectionCS::AnswerRequest 和 core_service 进行交互了。GetCore 会根据 slotId 得到对应的 Core,然后用 Get 函数得到对应的 event,设置 Owner 为 CellularCallHandler,等待返回时进行回调对应的 handler。
这里就完成了 Answer 流程在 cellular_call 的调用。
3.2.2.3 Answer事件在core_servicel中的处理
这部分的代码会调用 core 对应的 Answer 函数,消息就会传递到负责核心服务与 RIL Adapter 通信交互的 tel_ril 代码中。
进一步到负责 tel_ril 的管理类 TelRilManager 中,判断下对应的 telRilCall_ 是否为空,如果不为空就可以继续调用了。
telRilCall_ 是在 TelRilManager 的初始化中进行的。其中 InitCellularRadio 首先通过 ServiceManager 拿到 cellular_radio1 对应的 service。然后在 InitTelInfo 中用 cellularRadio_ 和 observerHandler_ 构造 telRilCall_。
调用到 teRilCall 的 Answer 函数进行处理,用 CreateTelRilRequest 构造 telRilRequest ,通过 TelRilBase 的 SendInt32Event 发出。
从调用的情况来看,则又通过 IPC 的 SendRequest 请求将 dispatchId 和 data 发给 cellular_radio1。
到这里 Answer 流程在 core_service 服务中的处理就完毕了。
3.2.2.4 Answer事件在ril_adapter中的处理
从目前的代码调用来看有可能是从 vendor 调用 hril_hdf 的 dispatch,然后传递消息到 ril_adapter 中
下边为 hril_hdf 的初始化过程。
可以看到 hdf 的初始化包括 MODULE_ NAME、RilAdapterBind、RilAdapterInit。
Bind 过程如下。
可以看看到 service 的 dispatch 对应于 RilAdapterDispatch。
Init 过程如下:
其中 LoadVendor 会加载对应 vendor 的 rilLib。
之前已经说到有可能是 vendor 调用了 dispatch,现在已经绑定了对应的 RilAdapterDispatch,后续处理如下。
C++ 写的 IPC 和 C 语言写的 IPC 是可以相互通信的,格式并不同。HdfSBuf 也可以和 MessageParcel 互转。
Request 消息通过 IPC 传到 RilAdapter 后会通过 DispatchRequest 分发出去。
在后续调用中我们知道 code 为 HREQ_CALL_ANSWER,所以会调用 DispatchModule。
根据 slotId 拿到对应 g_manager 的 Dispatch 函数。
由于我们的 code = 4,在 0 到 100 之间,所以会被判定为 callrequest
所以调用如下函数。
Request 如下。
调用 request 对应处理函数为 Answer。
首先要创建 HRilRequest,将信息封装进 requestInfo 中。
在 LoadVendor 时调用 HRilRegOps(ops) 时会进行 register。
这里会将 callOps 注册到 callFuncs_ 中,而 g_callBacks 实际是对应 hrilOps。
.callOps 对应于 &g_callReqOps,对应的函数对照表如下。
找对对应的 ReqAnswer 进行后续处理,这里其实已经走到 AT 命令的处理层。
后续调用 SendCommandLock 函数,这里就会将 requestInfo 处理成 AT 命令。
实际的处理在这个函数 SendCommandNoLock。
通过 WriteATCommand 将 AT 命令发给 modem,然后等待 modem 回复处理的结果。
到这里为止,Answer 流程已经完成了从 app 到 modem 的信息传递。
五、总结
从第三章的分析过程,我们已经完成了从 Call ui 响应来电提示,然后一步一步完成了整个框架层的调用过程,其实在结尾的时候我们只是完成了消息的下传到 modem,这个过程后 modem 也进行回复,这个过程也比较长此处就不再赘述了。有机会的话我们可以在后续的文章中进行分析。
整个文档由于本人能力和时间的限制,后续尽可能将遗漏地方补全。从 core_service 到 ril_adapter 的调用,是在 vendor 目录中将 cellular_radio1 的 libhril_hdf.z.so 加载。由于目前的代码还在完善中,有可能后续最新版的代码会有所改变。
高清流程图下载:https://ost.51cto.com/resource/1716
