在上一个版本中, 详细介绍了如何利用 广播与接收、Worker、Service 等实现电话的触发与状态变化的监听. 在这个基础之上, 实际使用中发现很不少可以优化的部分. 下面把比较重要的展示一下:
1. 首先是获取号码:
在高版本中, 获取号码需要传入卡槽id, 如果使用DEFAULT_SUBSCRIPTION_ID 默认ID的话, 会有一定的问题, 即当卡拔掉后, 获取的号码还是之前插卡对应的号码. 不能获取实时数据. 这在用户换卡时候容易造成歧义. 因此需要真实的当前卡槽1的ID, 这样每次请求号码就都是真实的. 完整代码:
@SuppressLint("MissingPermission")
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);
int[] ids = subscriptionManager.getSubscriptionIds(0);
int slot0_subId = (ids == null || ids.length == 0) ? SubscriptionManager.INVALID_SUBSCRIPTION_ID : ids[0];
Log.d(TAG, "slot0 id: "+ slot0_subId);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
line1Num = subscriptionManager.getPhoneNumber(slot0_subId);
}else{
SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(0);
line1Num = info == null ? "" : info.getNumber();
}
}
Log.i(TAG, "line1Number:"+line1Num);
return line1Num == null ? "" : line1Num;
}
2. 如何获取通话记录及录音:
之前的逻辑是在电话监听到挂断的时候, 延时调用getContentResolver查询最新一通录音. 这个逻辑能用,但不是最好的, 后来发现ContentResolver是可以注册回调的(Observer), 这就方便多了, 在服务启动的时候就可以注册上, 然后一旦录音生成了, 就会自动触发回调(Observer). 然后在后续的研究中,发现除了通话记录, 录音也可以通过这样的方式来获取到(但很多手机这里录音后可能没有写ContentResolver数据,导致无法如此查询, 目前发现可以完美适配的有OPPO的Color OS 12+). 详细代码展示:
private void monitorCallLogChange(Context context) {
// 获取ContentResolver实例
ContentResolver contentResolver = context.getContentResolver();
// 注册一个观察者来监听通话记录的变化
contentResolver.registerContentObserver(CallLog.Calls.CONTENT_URI, false, new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
Cursor cursor = null;
try {
// 查询最新的通话记录
cursor = contentResolver.query(CallLog.Calls.CONTENT_URI, null, null, null, CallLog.Calls.DATE + " DESC");
int numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER);
int typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE);
int dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE);
int durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION);
int idindex = cursor.getColumnIndex(CallLog.Calls._ID);
int lastmodifiedIndex = cursor.getColumnIndex(CallLog.Calls.LAST_MODIFIED);
if (cursor != null && cursor.moveToFirst()) {
/*
int i = cursor.getColumnCount();
for (int j = 0; j < i; j++) {
int type = cursor.getType(j);
if(type < 4)
Log.i(TAG, "CallLog: " + cursor.getColumnName(j)+":" + cursor.getString(j));
else
Log.i(TAG, "CallLog: " + cursor.getColumnName(j)+":" + cursor.getBlob(j).length);
}
*/
String phoneNumber = cursor.getString(numberIndex);
String callDate = cursor.getString(dateIndex);
int callType = cursor.getInt(typeIndex);
long callDuration = cursor.getInt(durationIndex);
int id = cursor.getInt(idindex);
long lastmodified = cursor.getLong(lastmodifiedIndex);
String uuid = sharedPreferences.getString("call_uuid", "");
Log.d(TAG, "uuid:"+uuid+"| modified:"+lastmodified/1000+"| phoneNumber:"+phoneNumber+" |callType:"+callType+" |callDate:"+callDate+" |callDuration:"+callDuration);
Data.Builder dataBuilder = new Data.Builder();
dataBuilder.putString("action",CRMWorker.localBroadcastActionForCallLog);
dataBuilder.putString("call_uuid",uuid);
dataBuilder.putString("callType",String.valueOf(callType));
dataBuilder.putString("date_modified",String.valueOf(lastmodified/1000));
dataBuilder.putString("callDuration",String.valueOf(callDuration));
dataBuilder.putString("callNumber",phoneNumber);
CRMWorker.Sync2CRM(context,dataBuilder.build());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
});
contentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, new ContentObserver(null) {
@Override
public void onChange(boolean selfChange,@Nullable Uri uri, int flags) {
super.onChange(selfChange,uri,flags);
Log.i(TAG, "MediaStore [External] Audio onChange: " + uri +"| flags:"+flags);
String media_id = uri.getLastPathSegment();
Log.i(TAG, "ID: " + media_id);
if(flags == ContentResolver.NOTIFY_UPDATE || flags == 0) { //finish writing files
String selection = MediaStore.Audio.Media._ID + " = ?"
// +" and " + MediaStore.Audio.Media.IS_RECORDING + "= ?"
;
String[] selectionArgs = new String[]{
String.valueOf(media_id),
//String.valueOf(1)
};
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, null);
if (cursor != null) {
int filePath_index = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
int modified_index = cursor.getColumnIndex(MediaStore.Audio.Media.DATE_MODIFIED);
if (cursor.moveToFirst()) {
/*
int i = cursor.getColumnCount();
for (int j = 0; j < i; j++) {
int type = cursor.getType(j);
if(type < 4)
Log.i(TAG, "Recording: " + cursor.getColumnName(j)+":" + cursor.getString(j));
else
Log.i(TAG, "Recording: " + cursor.getColumnName(j)+":" + cursor.getBlob(j).length);
}
*/
String filePath = cursor.getString(filePath_index);
long modified = cursor.getLong(modified_index);
String uuid = sharedPreferences.getString("call_uuid", "");
Log.d("Recordings", "call_uuid:"+uuid+", modified: " + modified + ", Path: " + filePath);
Data.Builder dataBuilder = new Data.Builder();
dataBuilder.putString("action",CRMWorker.localBroadcastActionForCallRecording);
dataBuilder.putString("call_uuid",uuid);
dataBuilder.putString("date_modified",String.valueOf(modified));
dataBuilder.putString("filePath",filePath);
CRMWorker.Sync2CRM(context,dataBuilder.build());
}
cursor.close();
}else{
Log.d("Recordings", "no record");
}
}
}
});
}
Calllog 与 Record 对应问题:
之前只有Callog, 可以直接提交CRM入库就完了. 但现在有了录音, 就不能这么粗暴对待了. 因为Calllog 与 Record 都是被动触发的. 如果当电话未接听(有Calllog, 无Record), 后面又打了一次接听的, 产生的record 就不好对应了. 如果录音再比calllog先出现的话, 就更乱套了. 所以现在需要一个ID来关联 Calllog 与 record. 这个ID也是CRM侧需要的, 当发起一通外呼时, CRM要先生成一条Calllog记录, 然后等待手机端回来数据更新. 所以这个ID对应很重要.
CRM主动发起外呼或手机拨号-> 手机发起呼叫 -> 挂机 ->CallLog [+ record]
手机被叫响铃 -> 发送来电消息到CRM实现弹屏-> 手机接听或挂机 -> CallLog [+ record]
主叫与被叫, 要找机会向CRM请求通话ID, 可以在第二部完成, 外呼的呼叫阶段和被叫响铃阶段. 这两个正好是电话监听的两个状态, 完整代码:
int callState = 0; // 0:待机/挂机 1:响铃 2:外呼 3:接听
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(PHONE_STATE_RECEIVED)) {
// 如果是来电
String incoming_number = intent.getExtras().getString("incoming_number");
if(incoming_number == null) return;
Log.i(TAG, "onReceive: "+intent.getAction());
if(tManager == null) tManager = (TelephonyManager) context.getSystemService(Service.TELEPHONY_SERVICE);
//电话的状态
int lastCallState = callState;
Log.i(TAG, "lastCallState: " + lastCallState);
switch (tManager.getCallState()) {
case TelephonyManager.CALL_STATE_RINGING:
callState = 1;
Log.i(TAG, "Ringing:"+incoming_number);
CRMWorker.Sync2CRM(context,getIncomingCall(incoming_number));
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
//接听状态
if(lastCallState == 0) {
callState = 2;
Log.i(TAG, "CallOut:"+incoming_number);
CRMWorker.Sync2CRM(context, getCallOutData(incoming_number));
}else {
callState = 3;
Log.i(TAG, "Answer:"+incoming_number);
}
break;
case TelephonyManager.CALL_STATE_IDLE:
//挂断状态
callState = 0;
Log.i(TAG, "Hangup:"+incoming_number);
break;
}
}
else if(intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)){
Log.i(TAG, "onReceive: "+intent.getAction());
Log.i(TAG, "BOOT_COMPLETED启动服务:TelephonyManagerService");
if(!TelephonyManagerService.isServiceRunning(context, TelephonyManagerService.class)) {
context.startService(new Intent(context, TelephonyManagerService.class));
}
}
}
通过这两个事件, 调用Worker请求CRM, 回来的数据再通过本地广播发给service, 去标记全局的通话ID(我用的是SharedPreferences). 这样就实现了, 在电话打通之前先获取到与CRM同步的通话ID, 后面CallLog 和 Record 拿到后, 就可以带着这个ID发给CRM去更新对应的记录了.
最后还有一个意外惊喜, 手机开启OTG功能后, 可以使用主动降噪的USB耳机. 完美适配了😄