业务需求:
需要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)) {
if(tManager == null) tManager = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE);
//电话的状态
String incoming_number="";
switch (tManager.getCallState()) {
case TelephonyManager.CALL_STATE_RINGING:
incoming_number = intent.getExtras().getString("incoming_number");
if(incoming_number == null) {
break;
}else{
Log.i(TAG, "Ringing:"+incoming_number);
//TODO. Sync2CRM then sendLocalBroadcast
}
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
//接听状态
incoming_number = intent.getExtras().getString("incoming_number");
if(incoming_number == null) {
break;
}else{
Log.i(TAG, "Answer:"+incoming_number);
}
break;
case TelephonyManager.CALL_STATE_IDLE:
//挂断状态
incoming_number = intent.getExtras().getString("incoming_number");
if(incoming_number == null) {
break;
}else{
Log.i(TAG, "Hangup:"+incoming_number);
// 等callLog完成写入再获取同步
new Handler().postDelayed( ()-> {
//TODO. getLastCall & Sync2CRM
}, 1000);
}
break;
}
}
else if(intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)){
Log.i(TAG, "BOOT_COMPLETED启动服务:TelephonyManagerService");
if(!TelephonyManagerService.isServiceRunning(context, TelephonyManagerService.class)) {
context.startService(new Intent(context, TelephonyManagerService.class));
}
}
}
获取CallLog最后通话记录, 返回Data数据类型是用于Worker. 里面action参数,是用于Worker执行不同逻辑并在完毕后发送本地广播.
private Data getLastCall(Context context){
Data.Builder dataBuilder = new Data.Builder();
Cursor cursor = context.getContentResolver().query(CallLog.Calls.CONTENT_URI,
null, null, null, CallLog.Calls.DATE + " DESC");
int number = cursor.getColumnIndex(CallLog.Calls.NUMBER);
int type = cursor.getColumnIndex(CallLog.Calls.TYPE);
int date = cursor.getColumnIndex(CallLog.Calls.DATE);
int duration = cursor.getColumnIndex(CallLog.Calls.DURATION);
if (cursor.moveToFirst()) {
/*
int i = cursor.getColumnCount();
for (int j = 0; j < i; j++) {
Log.i(TAG, "Call: " + cursor.getColumnName(j)+":"+cursor.getString(j));
}
*/
String phoneNumber = cursor.getString(number);
String callType = cursor.getString(type);
String callDate = cursor.getString(date);
int callDuration = cursor.getInt(duration);
// 处理通话记录
Log.d(TAG, "phoneNumber:"+phoneNumber+" |callType:"+callType+" |callDate:"+callDate+" |callDuration:"+callDuration);
dataBuilder.putString("action",CRMWorker.localBroadcastActionForCallLog);
dataBuilder.putString("callNumber",phoneNumber);
dataBuilder.putString("callType",callType);
dataBuilder.putString("callDate",callDate);
dataBuilder.putString("callDuration",String.valueOf(callDuration));
}
cursor.close();
return dataBuilder.build();
}
调用Worker
public static void Sync2CRM(Context context, Data inputData){
// 创建工作请求并添加输入数据
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(CRMWorker.class)
.setInputData(inputData)
.build();
// 将工作请求加入到WorkManager
WorkManager.getInstance(context)
.enqueue(workRequest);
}
Worker 响应操作 doWork()
public Result doWork() {
Data inputData = getInputData();
if (inputData != null) {
// 从输入数据中获取参数
Request request = null;
String action = inputData.getString("action");
if(action.equals(localBroadcastActionForCallLog)){
String callNumber = inputData.getString("callNumber");
String callType = inputData.getString("callType");
String callDate = inputData.getString("callDate");
String callDuration = inputData.getString("callDuration");
// 使用参数执行任务
Log.d(TAG, "doWork: " + line1Num + " -> "+ callNumber+"|"+callType+"|"+callDate+"|"+callDuration);
String url = "https://<domain>/api/appCallLog";
FormBody formBody = new FormBody.Builder()
.add("callNumber",callNumber)
.add("callType",callType)
.add("callDate",callDate)
.add("callDuration",callDuration)
.add("callerNumber", line1Num)
.build();
request = new Request.Builder()
.url(url)
.post(formBody)
.build();
}
else if(action.equals(localBroadcastActionForIncomingCall)){
String url = "https://<domain>/api/appIncomingCall";
String callNumber = inputData.getString("incoming_number");
FormBody formBody = new FormBody.Builder()
.add("incoming_number",callNumber)
.add("line_number", line1Num)
.build();
request = new Request.Builder()
.url(url)
.post(formBody)
.build();
}
else if(action.equals(localBroadcastActionForBindClient)){
String client_id = inputData.getString("client_id");
//String lineNum = inputData.getString("lineNum");
String url = "https://<domain>/api/appCallBindUser";
FormBody formBody = new FormBody.Builder()
.add("client_id",client_id)
.add("lineNum", line1Num)
.build();
request = new Request.Builder()
.url(url)
.post(formBody)
.build();
}
if(request != null)
{
OkHttpClient okHttpClient = new OkHttpClient();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG, "SyncCRM Failure:"+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
Log.d(TAG, action + "SyncCRM onResponse: " + result);
try {
JSONObject jsonObject = new JSONObject(result);
String status = jsonObject.getString("status");
String msg = jsonObject.getString("msg");
Log.i(TAG, action +" SyncCRM Response: "+ status);
Bundle data = new Bundle();
data.putString("status",status);
data.putString("msg",msg);
sendLocalBroadcast(action,data);
}catch(Exception e){
Log.d(TAG, action + "SyncCRM jsonErr: " + e.getMessage());
}
}
});
return Result.success();
}
}
return Result.failure();
}
发送本地广播
private void sendLocalBroadcast(String action, Bundle data){
Intent intent = new Intent(action);
if(data != null) {
intent.putExtras(data);
}
Log.i(TAG, "sendLocalBroadcast: " + action);
LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
}
Activity 绑定Service, 动态集中申请权限, 本地广播注册...
public class MainActivity extends AppCompatActivity {
//service class
private TelephonyManagerService mService;
String[] permissions;
List<String> mPermissionList = new ArrayList<>();
private boolean isBound = false;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
TelephonyManagerService.LocalBinder binder = (TelephonyManagerService.LocalBinder) service;
mService = binder.getService();
isBound = true;
Log.i(TAG, "onServiceConnected");
//UpdateUIFromService(mService.isLogin());
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
mService = null;
isBound = false;
}
};
// 接收本地广播的接收器
private BroadcastReceiver mServiceMessageReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
Log.i(TAG, "Activity BroadcastReceiver: " + action);
switch (action){
case CRMWorker.localBroadcastActionForBindClient:
//UpdateUIFromService(extras.getString("status").equals("200"));
break;
case CRMWorker.localBroadcastActionForCallLog:
Toast.makeText(MainActivity.this,extras.getString("msg"),Toast.LENGTH_SHORT).show();
}
}
};
//权限集中放在一起, 方便增减
public MainActivity() {
permissions = new String[]{
Manifest.permission.CALL_PHONE,
Manifest.permission.PROCESS_OUTGOING_CALLS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_CALL_LOG,
Manifest.permission.RECEIVE_BOOT_COMPLETED,
Manifest.permission.READ_PHONE_NUMBERS,
Manifest.permission.MANAGE_OWN_CALLS,
Manifest.permission.FOREGROUND_SERVICE,
Manifest.permission.FOREGROUND_SERVICE_PHONE_CALL,
};
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注册本地广播接收器
IntentFilter localFilter = new IntentFilter();
localFilter.addAction(CRMWorker.localBroadcastActionForBindClient);
localFilter.addAction(CRMWorker.localBroadcastActionForCallLog);
LocalBroadcastManager.getInstance(this).registerReceiver(
mServiceMessageReceiver, localFilter);
//授权完毕再开启服务
if (checkPermission()) {
startService();
}
}
private void startService() {
String line1Num = TelephonyManagerService.getLine1Num(this,TAG);
if(line1Num != null && !line1Num.isEmpty()){
//conStatus.setText(line1Num);
Intent startServiceIntent = new Intent(this,TelephonyManagerService.class);
startServiceIntent.putExtra("line1Num",line1Num);
startService(startServiceIntent);
//mLine1Num = line1Num;
}
}
@Override
protected void onStart() {
super.onStart();
Log.i(TAG, "onStart");
bindService(new Intent(this, TelephonyManagerService.class),connection,Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
if (isBound) {
unbindService(connection);
isBound = false;
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy");
//destory时不 stopService, 因为服务允许脱离activity运行.
}
private boolean checkPermission() {
mPermissionList.clear();
for (int i = 0; i < permissions.length; i++) {
if (ActivityCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED) {
mPermissionList.add(permissions[i]);
}
}
if (mPermissionList.isEmpty()) {//未授予的权限为空,表示都授予了
return true;
} else {//请求权限方法
String[] permissions = mPermissionList.toArray(new String[mPermissionList.size()]);//将List转为数组
ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST);
return false;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
mCheckedPermission = true;
if(requestCode == 1)
startService();
}
//打开app setting UI 用于用户手动配置服务自启动和横幅通知
private void naviToSetting(){
Uri packageURI = Uri.parse("package:"+getPackageName());
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,packageURI);
startActivity(intent);
}
}