返回 1. 使用
Android通话状态监听失效问题分析与解决方案(2024)
Android
2024-12-17 14:06:17
Android通话状态监听失效问题分析与解决
安卓系统中,监听通话状态变化曾是一种常见需求,开发者借此实现通话录音、来电弹屏等功能。但随着安卓系统权限收紧及隐私保护增强,原有的通话状态监听方式在2024年面临挑战。
问题现象
开发者此前(2023年10月)编写的BroadcastReceiver
用于监听通话结束事件,并提取通话时长、类型等信息上传服务器。该代码在Android 13设备上测试失效,BroadcastReceiver
的onReceive
方法未被触发。代码已包含必要权限声明,并排除多BroadcastReceiver
冲突可能。
原因分析
安卓系统对通话状态监听的限制主要体现在以下方面:
- 权限收紧:
READ_PHONE_STATE
权限的授予更加严格,即使应用获取该权限,后台监听行为也可能被系统限制。 - 后台执行限制: Android系统对后台任务执行进行了诸多限制,以优化电池续航和提升用户体验。
BroadcastReceiver
作为一种在后台运行的组件,其行为易受系统管控。特别是针对隐式广播,Android 13 及更高版本对其进行了严格限制。 - 隐式广播限制:
android.intent.action.PHONE_STATE
属于隐式广播,从Android 8.0(API 级别26)开始,系统对隐式广播进行了限制,以提高系统性能和用户体验。应用必须显式注册,动态注册方式的优先级可能不足以保证BroadcastReceiver
正常工作。
解决方案
1. 使用TelephonyManager.listen
针对PHONE_STATE
广播失效,推荐使用 TelephonyManager.listen
方法监听通话状态。该方法允许应用注册一个 PhoneStateListener
来监听特定的通话事件,无需在Manifest中声明BroadcastReceiver
,减少被系统限制的可能性。
代码示例:
import android.content.Context;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class CallStateMonitor {
private static final String TAG = "CallStateMonitor";
private final Context context;
private final TelephonyManager telephonyManager;
private MyPhoneStateListener phoneStateListener;
public CallStateMonitor(Context context) {
this.context = context;
telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
}
public void startMonitoring() {
Executor executor = Executors.newSingleThreadExecutor(); // Or use main looper with context.getMainExecutor()
phoneStateListener = new MyPhoneStateListener();
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
public void stopMonitoring() {
if (phoneStateListener != null) {
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
phoneStateListener = null;
}
}
private class MyPhoneStateListener extends PhoneStateListener {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_IDLE:
Log.d(TAG, "Call State Idle, Incoming Number: " + incomingNumber );
// Call ended, get call details
if(incomingNumber != null && !incomingNumber.isEmpty()){
getCallDetails(incomingNumber);
}else {
Log.d(TAG, "No Incoming number found.");
}
break;
case TelephonyManager.CALL_STATE_RINGING:
Log.d(TAG, "Call State Ringing, Incoming Number: " + incomingNumber);
// Incoming call ringing
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
Log.d(TAG, "Call State Offhook");
// Call started (either outgoing or incoming answered)
break;
}
}
}
private void getCallDetails( String phone) {
CallLogHelper callLogHelper = new CallLogHelper(context);
callLogHelper.getCallDetails(phone, callLogModels -> {
if (callLogModels != null && !callLogModels.isEmpty()) {
for(CallLogModel callLog : callLogModels){
Log.d(TAG, "Call Details " + "Number:" + callLog.getPhoneNumber()
+ " ,Call Type: " + callLog.getCallType()
+ " ,Call DayTime: " + callLog.getCallDayTime()
+ " ,Call Duration: " + callLog.getCallDuration() );
}
}
else {
Log.d(TAG, "No CallLogModel details for phone "+ phone);
}
});
}
}
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.provider.CallLog;
import android.util.Log;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class CallLogHelper {
private Context context;
private final static String TAG = "CallLogHelper";
public CallLogHelper(Context context){
this.context = context;
}
public interface CallLogListener{
void onCallLogReceived(List<CallLogModel> callLogModel);
}
public void getCallDetails(String phone, CallLogListener callLogListener) {
List<CallLogModel> callLog = new ArrayList<>();
long timestamp = getTodayTimestamp();
new Thread(()->{
Cursor managedCursor = context.getContentResolver().query ( CallLog.Calls.CONTENT_URI,
null,CallLog.Calls.DATE + ">= ? AND " + CallLog.Calls.NUMBER + " = ?" ,
new String[]{String.valueOf(timestamp) ,phone}, CallLog.Calls.DATE + " DESC");
int number = managedCursor.getColumnIndex( CallLog.Calls.NUMBER );
int type = managedCursor.getColumnIndex( CallLog.Calls.TYPE );
int date = managedCursor.getColumnIndex( CallLog.Calls.DATE);
int duration = managedCursor.getColumnIndex( CallLog.Calls.DURATION);
if ( managedCursor.moveToFirst() ) {
do {
String phNumber = managedCursor.getString( number );
String callType = managedCursor.getString( type );
String callDate = managedCursor.getString( date );
Date callDayTime = new Date(Long.parseLong(callDate));
String callDuration = managedCursor.getString( duration );
String dir = null;
int dircode = Integer.parseInt( callType );
switch( dircode ) {
case CallLog.Calls.OUTGOING_TYPE:
dir = "OUTGOING";
break;
case CallLog.Calls.INCOMING_TYPE:
dir = "INCOMING";
break;
case CallLog.Calls.MISSED_TYPE:
dir = "MISSED";
break;
}
CallLogModel callLogModel = new CallLogModel();
callLogModel.setPhoneNumber(phNumber);
callLogModel.setCallType(dir);
callLogModel.setCallDayTime(String.valueOf(callDayTime));
callLogModel.setCallDuration(callDuration);
callLog.add(callLogModel);
} while(managedCursor.moveToNext());
managedCursor.close();
callLogListener.onCallLogReceived(callLog);
}else {
Log.d(TAG, "No call log found for the number: " + phone);
callLogListener.onCallLogReceived(null);
}
}).start();
}
public long getTodayTimestamp(){
Calendar c1 = Calendar.getInstance();
c1.setTime(new Date());
Calendar c2 = Calendar.getInstance();
c2.set(Calendar.YEAR, c1.get(Calendar.YEAR));
c2.set(Calendar.MONTH, c1.get(Calendar.MONTH));
c2.set(Calendar.DAY_OF_MONTH, c1.get(Calendar.DAY_OF_MONTH));
c2.set(Calendar.HOUR_OF_DAY, 0);
c2.set(Calendar.MINUTE, 0);
c2.set(Calendar.SECOND, 0);
return c2.getTimeInMillis();
}
}