OpenHarmony 通话应用源码剖析 原创 精华

深开鸿
发布于 2022-4-28 18:49
浏览
2收藏

作者:赖尧

一、简介

​ 通话应用主要提供通话相关用户交互界面,根据电话服务子系统提供的通话数据和状态显示语音去电界面、语音来电界面、语音通话界面、语音多方通话界面、会议通话界面、会议管理界面;并根据用户界面上的操作完成接听、挂断、拒接、静音、保持、音频通道切换、DTMF键盘指令等下发电话服务子系统。

二、架构图

OpenHarmony 通话应用源码剖析-鸿蒙开发者社区

三、代码结构

/applications_call
├── callui                                       # 通话应用主Ability,提供拉起应用入口
│   └── src
│       └── main
│           ├── ets                              # ets代码目录
│               ├── default
│                   ├── assets                   # 图片资源
│                   ├── common                   # 公共组件或方法配置目录
│                       ├── components           # 公共组件
│                       ├── configs          	 # 应用配置对象目录
│                       ├── constant             # 应用常量对象目录
│                       ├── utils                # 公共方法
│                   ├── model                    # Model层代码目录
│                   ├── pages                    # 通话页面目录
|                   ├── app.ets                  # 全局ets逻辑和应用生命周期管理文件
│               ├── ServiceAbility               # 服务ability
│                   ├── callManagerService.ets   # ServiceAbility方法
│                   ├── service.ts               # ServiceAbility方法
│                   ├── telephonyApi.ets         # ServiceAbility方法
│           ├── resources                        # 资源配置文件存放目录
|               ├── base                         # 默认图片资源,字体大小,颜色资源存放目录
|               ├── zh_CN                        # 中文语言场景资源内容存放目录
│           ├── config.json                      # 全局配置文件
├── figures                                      # 架构图目录
│   └── callui_en.png                            # 架构设计图
├── signature                                    # 签名证书文件目录
│   └── com.ohos.callui.p7b                      # 签名文件
├── LICENSE                                      # 许可证

四、流程图

OpenHarmony 通话应用源码剖析-鸿蒙开发者社区

五、时序图

OpenHarmony 通话应用源码剖析-鸿蒙开发者社区

六、源码分析

1、启动通话常驻服务

开机启动 通话应用常驻服务PA, 由元能力子系统拉起 代码路径/foundation/aafwk/standard/services/abilitymgr/src/ability_manager_service.cpp

void AbilityManagerService::StartingPhoneServiceAbility()
{
    HILOG_DEBUG("%{public}s", __func__);
    auto bms = GetBundleManager();
    CHECK_POINTER_IS_NULLPTR(bms);

    AppExecFwk::AbilityInfo phoneServiceInfo;
    Want phoneServiceWant;
    phoneServiceWant.SetElementName(AbilityConfig::PHONE_SERVICE_BUNDLE_NAME,
        AbilityConfig::PHONE_SERVICE_ABILITY_NAME);

    auto userId = GetUserId();
    int attemptNums = 1;
    HILOG_DEBUG("%{public}s, QueryAbilityInfo, userId is %{public}d", __func__, userId);
    IN_PROCESS_CALL_WITHOUT_RET(
        while (!(bms->QueryAbilityInfo(phoneServiceWant,
            OHOS::AppExecFwk::AbilityInfoFlag::GET_ABILITY_INFO_DEFAULT, userId, phoneServiceInfo)) &&
            attemptNums <= MAX_NUMBER_OF_CONNECT_BMS) {
            HILOG_INFO("Waiting query phone service ability info completed.");
            usleep(REPOLL_TIME_MICRO_SECONDS);
            attemptNums++;
        }
    );

    (void)StartAbility(phoneServiceWant, userId, DEFAULT_INVAL_VALUE);
}
2、服务注册监听

service ability 应用启动加载入口文件service.ts文件, 执行onStart钩子函数, 实例化CallManagerService类时添加注册监听, registerCallStateCallback 调用注册电话子系统接口function on(type: ‘callDetailsChange’, callback: Callback<CallAttributeOptions>): void;

添加注册监听子系统上报的状态

 /**
   * add register listener
   */
  addRegisterListener() {
    this.mTelephonyCall.registerCallStateCallback(this.getCallData.bind(this));
  }
 
 public registerCallStateCallback(callBack) {
    call.on('callDetailsChange', (data) => {
      if (!data) {
        HiLog.i(TAG,prefixLog + 'call.on registerCallStateCallback' + JSON.stringify(data))
        return;
      }
      HiLog.i(TAG,prefixLog + 'call.on registerCallStateCallback callState: ' + JSON.stringify(data.callState))
      callBack(data);
    });
  }

根据上报通话当前状态校验是来电还是去电如果是就做拉起操作, 否则通话发布公共事件;

getCallData(callData) {
    this.callData = callData;
    this.updateCallList();
    const {callState} = this.callData;

    /**
     * single call or dialing pull up the application
     */
    if ((callState === CALL_STATUS_INCOMING && this.callList.length === 1) || callState === CALL_STATUS_DIALING) {
      this.startAbility(callData);
    } else if (callState !== CALL_STATUS_DISCONNECTING) {
      this.publishData(callData);
    }
 }
 publishData(callData) {
    commonEvent.publish('callui.event.callDetailsChange', {
      bundleName: CALL_BUNDLE_NAME,
      isOrdered: false,
      data: JSON.stringify(callData)
    }, (res) => {
      HiLog.i(TAG, "callUI service commonEvent.publish callback res : %s")
    });
  }
3、服务注册公共事件广播

在启动注册监听后同时也注册添加公共事件广播, 其中’callui.event.callEvent’事件监听通话FA获取初始化数据, ‘callui.event.click’ 事件监听systemui 通知栏操作按钮事件;

 const events = ['callui.event.callEvent', 'callui.event.click'];

 async addSubscriber() {
    subscriber = await new Promise((resolve) => {
      commonEvent.createSubscriber({
        events
      }, (err, data) => {
        HiLog.i(TAG, "addSubscriber  %s")
        resolve(data);
      });
    });

    commonEvent.subscribe(subscriber, (err, res) => {
      if (err.code === 0) {
        if (res.event === events[0]) {
          const obj = JSON.parse(res.data);
          if (obj && obj.key === 'getInitCallData') {
            this.publishData(this.callData);
          }
        }

        if (res.event === events[1]) {
          const {callId,btnType} = res.parameters
          this.btnclickAgent(callId, btnType)
        }
      } else {
        HiLog.i(TAG, "callui service commonEvent.subscribe failed err : %s" + JSON.stringify(err))
      }
      subscriber.finishCommonEvent()
        .then(() => {
        HiLog.i(TAG, "addSubscriber finishCommonEvent : %s")
      })
    });
  }
4、拉起通话应用初始化数据

在service中通过 PA.startAbility方法拉起 通话FA应用, 通话应用FA在启动入口页面index, 实例化CallManager类,调用initCallData方法, 获取初始通话数据, 调用update更新通话状态;

 private initCallData() {
    featureAbility.getWant().then((want) => {
      if (want && want.parameters && ('callState' in want.parameters)) {
        this.update(want.parameters);
        HiLog.i(TAG, "initCallData featureAbility.getWant :  %s")
      } else {
        this.mCallServiceProxy.publish({
          key: 'getInitCallData',
          params: []
        });
      }
    })
      .catch((error) => {
        HiLog.i(TAG, "initCallData catch error :  %s" + JSON.stringify(error))
      });
  }
5、更新通话状态

在CallManger类实例化时候, 添加公共事件广播监听’callui.event.callDetailsChange’, 应用中订阅到数据调用callDataManager中的update方法,更新callData和callList校验通话状态, 是单方通话或者是多方通话;

 const events = ['callui.event.callDetailsChange'];
 private async registerSubscriber() {
    subscriber = await new Promise((resolve) => {
      commonEvent.createSubscriber({
        events
      },
        (err, data) => {
          resolve(data);
        }
      );
    });

    commonEvent.subscribe(subscriber, (err, res) => {
      if (err.code === 0) {
        const callData = JSON.parse(res.data);
        this.callData = callData
        HiLog.i(TAG, "commonEvent subscribe : %s")
        if (callData) {
          this.update(callData);
        }
        HiLog.i(TAG, "commonEvent subscribe : %s")
      } else {
        HiLog.i(TAG, "commonEvent.subscribe err: %s" + JSON.stringify(err))
      }
    });
  }

async update(callData) {
    if (globalThis.permissionFlag) {
      await this.contactManager.getContactInfo(callData)
    }
    this.mCallDataManager.update(callData);
    call.formatPhoneNumber(callData.accountNumber, (err, data) => {
      if (data === undefined) {
        AppStorage.SetOrCreate("AccountNumber", callData.accountNumber)
      } else {
        AppStorage.SetOrCreate("AccountNumber", data)
      }
    });
    HiLog.i(TAG, "update : ")
  }
  
public update(callData) {
    const { callState, callId } = callData;
    const targetObj = this.callList.find((v) => v.callId === callId);
    HiLog.i(TAG, "update : ")
    if (targetObj) {
      Object.assign(targetObj, {
        ...callData
      });
    } else {
      this.addCallList({
        ...callData
      });
    }

    if (callData.callState === CallStateConst.CALL_STATUS_ACTIVE) {
      this.updateCallTimeList(callData);
    }
    const singleCallState = callState === CallStateConst.CALL_STATUS_ACTIVE ||
    callState === CallStateConst.CALL_STATUS_WAITING || this.callList.length === 1;
    const multiCallState = (callState === CallStateConst.CALL_STATUS_DIALING ||
    callState === CallStateConst.CALL_STATUS_ALERTING) && this.callList.length > 1;
    if (singleCallState || multiCallState) {
      this.mCallStateManager.update(callData);
      this.callStateChange(callState);
    }

    if (callState === CallStateConst.CALL_STATUS_DISCONNECTED) {
      if (this.callList.length === 1) {
        this.NotificationManager.cancelNotification();
        AppStorage.Get<NotificationManager>('notificationManager').sendCapsuleNotification(callData, true);
        app.terminate();
      } else {
        this.removeCallById(callId);
        const activeCallData = this.callList.find((v) => v.callState === CallStateConst.CALL_STATUS_ACTIVE);
        if (activeCallData) {
          this.mCallStateManager.update(activeCallData);
          this.callStateChange(activeCallData);
        } else if (this.callList[0]) {
          this.mCallStateManager.update(this.callList[0]);
          this.callStateChange(this.callList[0].callState);
        }
      }
    }
  }

通话状态callState 包含的类型:

// calling
public static CALL_STATUS_ACTIVE: number = 0; // 通话中

// State keeping
public static CALL_STATUS_HOLDING: number = 1; // 保持

// Dialing
public static CALL_STATUS_DIALING: number = 2; // 正在拨号

// The other party is ringing
public static CALL_STATUS_ALERTING: number = 3; // 对方已振铃

// Call from the other party
public static CALL_STATUS_INCOMING: number = 4; // 来电

// Waiting for third-party calls
public static CALL_STATUS_WAITING: number = 5; // 三方来电等待

// Hung up
public static CALL_STATUS_DISCONNECTED: number = 6; // 已挂断

// Hanging up
public static CALL_STATUS_DISCONNECTING: number = 7; // 正在挂断
6、发布通知

在通话应用切换到后台, 会触发onPageHide钩子函数发送通知, 其中notificationManager.sendNotification方法发送按钮通知, notificationManager.sendCapsuleNotification方法发送的是胶囊通知;

发布按钮通知, 配置actionButtons, 其中消息类型contentType 等于 notification.ContentType.NOTIFICATION_CONTENT_LONG_TEXT, 表示长消息类型, want下的 action配置为’callui.event.click’, 点击通知按钮会触发systemUI 中的WantAgent.trigger 系统会发布公共事件’callui.event.click’,在通话service中订阅

公共事件’callui.event.click’响应的数据

onPageHide() {
    HiLog.i(TAG, "onPageHide :")
    this.appInactiveState = true;
    const {callState, accountNumber, contactName, callId} = this.callData;
    let fool = (callState !== CallStateConst.callStateObj.CALL_STATUS_DISCONNECTED && callId)
    if (callState !== CallStateConst.callStateObj.CALL_STATUS_DISCONNECTED && callId) {
      let text = contactName + '  ' + accountNumber + '  ';
      if (!contactName) {
        text = accountNumber + '  ' ;
      }
      this.notificationManager.sendNotification(text, this.callData);
      this.notificationManager.sendCapsuleNotification(this.callData, true);
      HiLog.i(TAG, "onPageHide end :  ")
    }
}

// 发布通知
  async sendNotification(text, callData) {
    const {callState, callId} = callData;
    const actionBtnKeys = this.getMapObj(callState) || [];
    const {START_ABILITY, SEND_COMMON_EVENT} = wantAgent.OperationType;
    const wantAgentObj = await this.getWantAgent(callData, START_ABILITY);
    notificationRequest.wantAgent = wantAgentObj;
    notificationRequest.actionButtons = [];
    if (actionBtnKeys.length) {
      for (const key of actionBtnKeys) {
        const data = {
          callId, btnType: key
        };
        const wantAgentObj = await this.getWantAgent(data, SEND_COMMON_EVENT);
        resourceManager.getResourceManager((error, mgr) => {
          if (error != null) {
            return;
          }
          mgr.getString(textMap[key].id, (error, value) => {
            if (error != null) {
            } else {
              notificationRequest.actionButtons.push({
                title: value,
                wantAgent: wantAgentObj
              });
              Object.assign(notificationRequest.content.longText, {
                title: text,
                expandedTitle: text
              });
              notification.publish(notificationRequest);
            }
          });
        });
      }
    }
    HiLog.i(TAG, "sendNotification end : ")
  }

// 发送胶囊通知
 sendCapsuleNotification(callData, isBackground) {
    HiLog.i(TAG, "sendCapsuleNotification isBackground : %s"  + JSON.stringify(isBackground))
    callData.startTime = (callData.startTime)
    HiLog.i(TAG, "sendCapsuleNotification callData.startTime  : ")
    const {callState, startTime} = callData;
    commonEvent.publish('CAPSULE_EVENT_CALL_UI', {
      bundleName: 'com.ohos.callui',
      isOrdered: false,
      data: JSON.stringify({
        callState,
        startTime: startTime*1000,
        isBackground,
        wantBundleName: CALL_BUNDLE_NAME,
        wantAbilityName: CALL_ABILITY_NAME
      })
    }, (res) => {
      HiLog.i(TAG, "callUI app commonEvent.publish CAPSULE_EVENT_CALL_UI callback res  : %s")
    });
  }

七、列举调用电话子系统接口

1、来电接听, 拒接;通话中挂断, 保持, 取消保持接口调用;


// 接听
public acceptCall = function (callId) {
  call.answer(callId).then((res) => {
    HiLog.i(TAG,prefixLog +  "call.answer : %s")
  }).catch((err) => {
    HiLog.i(TAG, prefixLog + "call.answer catch : %s" + JSON.stringify(err))
  });
};

// 拒接
 public rejectCall = function (callId, isSendSms = false, msg = '') {
 	// 普通拒接和短信拒接
    const rejectCallPromise = isSendSms ? call.reject(callId, {messageContent: msg}) : call.reject(callId);
    rejectCallPromise.then((res) => {
      HiLog.i(TAG,prefixLog +  "then:rejectCall : %s")
    })
      .catch((err) => {
        HiLog.i(TAG, prefixLog + "catch:rejectCall : %s" + JSON.stringify(err))
    });
  };
  
  // 挂断
  public hangUpCall = (callId) => new Promise((resolve, reject) => {
    call.hangup(callId).then((res) => {
      resolve(res);
      HiLog.i(TAG, prefixLog + "then:hangUpCall : %s")
    }).catch((err) => {
      reject(err);
      HiLog.i(TAG, prefixLog + "catch:hangUpCall : %s" + JSON.stringify(err))
    });
  });
  
  // 保持
  public holdCall = (callId) => new Promise((resolve, reject) => {
    call.holdCall(callId).then((res) => {
      resolve(res);
      HiLog.i(TAG,prefixLog +  "then:holdCall : %s")
    })
      .catch((err) => {
        reject(err);
        HiLog.i(TAG,prefixLog +  "catch:holdCall : %s" + JSON.stringify(err))
    });
  });
  
  // 取消保持
  public unHoldCall = (callId) => new Promise((resolve, reject) => {
    call.unHoldCall(callId).then((res) => {
      resolve(res);
      HiLog.i(TAG,prefixLog +  "then:unHoldCall : %s")
    })
      .catch((err) => {
        reject(err);
        HiLog.i(TAG,prefixLog +  "catch:unHoldCall : %s" + JSON.stringify(err))
    });
  });

2、拨号盘拨号接口调用

// 拨号
public dialCall(phoneNumber, accountId = 0, videoState = 0, dialScene = 0) {
  HiLog.i(TAG, "dialCall phoneNumber : ")
  return call.dial(phoneNumber, {
    accountId,
    videoState,
    dialScene
  });
}

更多原创内容请关注:深开鸿技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建HarmonyOS生态。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2023-2-2 10:19:33修改
2
收藏 2
回复
举报
3条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

赞,感谢整理!

回复
2022-4-29 09:38:34
L——奚
L——奚

call.on('callDetailsChange')这个api在3568上不回调,大佬你那边这个能调成功吗

回复
2022-5-19 18:19:36
keguanlaiyao
keguanlaiyao 回复了 L——奚
call.on('callDetailsChange')这个api在3568上不回调,大佬你那边这个能调成功吗

call.on('callDetailsChange')这个接口使用api7打包是ok的, 使用api8 打包项目, n-api底层ACE出现崩溃问题, OpenHarmony那边还没有解决

已于2022-5-24 09:46:12修改
回复
2022-5-24 09:39:45
回复
    相关推荐