业务需求:
需要CRM系统对接手机app, 让员工通过app连接到CRM系统, 让CRM下发呼叫指令, 让手机来电可以第一时间通知CRM弹屏客户资料信息.
开发目标:
1. 通过外部系统, 发送号码及指令到手机端, 让手机发起呼叫.
2. 手机呼入, 发送http请求外部系统, 告知什么号码来电.
3. (来电/去电)通话结束后, 采集通话记录发送到外部系统
4. 录音上传外部系统(未实现)
呼入呼出流程设计:
接收外部呼叫消息 -> 通过new Intent(Intent.ACTION_CALL, Uri.parse("tel:"+num))跳转到系统呼叫UI发起呼叫. 通话结束后, 利用广播接收器收到系统挂机消息, 获取通话记录通过http发到外部系统. 外呼流程结束.
手机来电开始响铃 - 广播接收器收到响铃消息, 通过http发送来电号码到外部系统. 挂机后, 接收器收到挂机消息, 获取通话记录发到外部系统. 呼入流程结束.
根据流程设计需要用到四个模块:
BroadcastReceiver: 接收器, 接收系统通知(响铃、挂机, 系统启动)
Service: app服务, 为了能在UI关闭后仍然运行, 大部分逻辑要在服务里实现.
Actitity: UI, 状态展示与输入
Worker: 耗时的网络请求都放在Worker里执行.
程序实现:
安装首次打开app, 会先集中授权所有权限, 当权限全部完成之后, 首先获取手机卡1号码(早期版本用TelephonyManager, 新版本用SubscriptionManager), 携带号码启动服务startService().
服务首次启动, 首先在onCreate 里注册广播接收器, 用于接收系统广播. 接着在onStartCommand里就可以创建通知频道并启动前台服务startForeground(). 为了满足后面没有UI的时候, 启动也能拿到手机号码, 需要把传入的手机号存入sharedPreferences里, 用于程序自启动时读取. 建立socket连接用于与外部系统通信.
等待socket连接成功, 通过Worker 完成http请求外部系统, 在成功回调可发送local广播通知前台UI更新状态展示.
----此时服务启动完成-----
外部系统通过websocket发送socket到手机, 需要手机调出拨号程序, 但android高版本权限限制原因, 不能直接从service 打开 activity, 因此采用通知方式解决:
用new Intent(Intent.ACTION_CALL, Uri.parse("tel:"+text) 创建的 PendingIntent生成一个notification, 这样点击通知的时候, 就可以直接唤出呼叫UI. 为了更明显的提醒, 需要通知使用铃声并且需要横幅通知. 这两项配置在代码里写了并不能直接使用, 需要用户手动在app设置里打开, 所以UI上需要一个按钮打开这个app setting UI.
电话呼入首先响应的是广播接收器, 通过action android.intent.action.PHONE_STATE 接收后通过 TelephonyManager实例调用 getCallState() 可以区分 响铃、接听和挂机. 在响铃的case中, 调用Worker 发送请求告知外部系统, 有电话呼入, 外部系统可以根据自己需要做处理(比如弹屏显示客户信息).
最后就是挂机, 一开始一直纠结直接通过广播来生成通话记录, 后来发现外呼的时候, 客户是否接听, 没有收到任何广播, 通过求助后得知可以用 CallLog可以查询到通话记录, 那就可以在每次挂机的时候, 读取最新一条通话记录数据. 猜测CallLog也是通过接收到挂机广播才写入数据的, 所以如果在接收挂机广播的时候, 立刻读取CallLog, 是查不到最新一条数据的. 但是CallLog写入完成又没查到有回调或者广播能感知, 所以只能用延时1秒调用的方式读取CallLog最新通话记录并通过worker发送至外部系统.
相关代码:
获取卡1号码
public static String getLine1Num(Context context, String TAG){
String line1Num;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
line1Num = telephonyManager.getLine1Number();
}else{
SubscriptionManager subscriptionManager = (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
line1Num = subscriptionManager.getPhoneNumber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
}else{
SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(0);
line1Num = info.getNumber();
}
}
Log.i(TAG, "line1Number:"+line1Num);
return line1Num;
}
广播接收OnReceive
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive: "+intent.getAction());
if(intent.getAction().equals(PHONE_STATE_RECEIVED)) {