android12 手机通话监听 Service Broadcast Worker Notification TelephonyManager SubscriptionManager

本文描述了一种通过CRM系统与手机应用集成的方法,涉及通过API发送呼叫指令、接收电话状态更新、通话结束后同步记录至CRM,以及处理Android权限和通话过程中的通知问题。
摘要由CSDN通过智能技术生成

业务需求: 

需要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);

    }

}

  • 31
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值