返回

Android通话状态监听失效问题分析与解决方案(2024)

Android

Android通话状态监听失效问题分析与解决

安卓系统中,监听通话状态变化曾是一种常见需求,开发者借此实现通话录音、来电弹屏等功能。但随着安卓系统权限收紧及隐私保护增强,原有的通话状态监听方式在2024年面临挑战。

问题现象

开发者此前(2023年10月)编写的BroadcastReceiver用于监听通话结束事件,并提取通话时长、类型等信息上传服务器。该代码在Android 13设备上测试失效,BroadcastReceiveronReceive方法未被触发。代码已包含必要权限声明,并排除多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();
    }

}