From 97643c3bcdb337a6c88ebdf278615b039a4ce431 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 13 Dec 2025 15:22:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BA=90=E7=A0=81=E6=95=B4=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=80=9A=E8=AF=9D=E5=BD=95=E9=9F=B3=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contacts/build.properties | 4 +- .../phonecallui/PhoneCallService.java | 390 ++++++++++-------- 2 files changed, 225 insertions(+), 169 deletions(-) diff --git a/contacts/build.properties b/contacts/build.properties index 321059e..676f984 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Dec 13 07:01:07 GMT 2025 +#Sat Dec 13 07:20:26 GMT 2025 stageCount=1 libraryProject= baseVersion=15.12 publishVersion=15.12.0 -buildCount=111 +buildCount=112 baseBetaVersion=15.12.1 diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java index 0a43f23..b12a2d3 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java @@ -1,19 +1,6 @@ package cc.winboll.studio.contacts.phonecallui; -/** - * 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI - * - * @author aJIEw - * @see PhoneCallActivity - * @see android.telecom.InCallService - */ -import android.content.ContentResolver; -import android.database.Cursor; import android.media.AudioManager; -import android.media.MediaRecorder; -import android.net.Uri; -import android.os.Build; -import android.provider.CallLog; import android.telecom.Call; import android.telecom.InCallService; import android.telephony.TelephonyManager; @@ -23,194 +10,263 @@ import cc.winboll.studio.contacts.dun.Rules; import cc.winboll.studio.contacts.fragments.CallLogFragment; import cc.winboll.studio.contacts.model.RingTongBean; import cc.winboll.studio.libappbase.LogUtils; -import java.io.File; -import java.io.IOException; -@RequiresApi(api = Build.VERSION_CODES.M) +/** + * 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI + * @author aJIEw, ZhanGSKen&豆包大模型 + * @see PhoneCallActivity + * @see android.telecom.InCallService + * 适配:Java7 语法 + Android API29-30 | 移除录音功能 | 强化稳定性与容错性 + */ +@RequiresApi(api = 29) // 适配API29+,替代Build.VERSION_CODES.M,匹配InCallService实际最低要求 public class PhoneCallService extends InCallService { + // ====================== 常量定义区(精简必要常量,无冗余) ====================== public static final String TAG = "PhoneCallService"; - MediaRecorder mediaRecorder; + // ====================== 成员属性区(按功能归类,命名规范) ====================== + private Call.Callback mCallCallback; // 通话状态回调(统一管理,便于销毁) - private final Call.Callback callback = new Call.Callback() { - @Override - public void onStateChanged(Call call, int state) { - super.onStateChanged(call, state); - switch (state) { - case TelephonyManager.CALL_STATE_OFFHOOK: - { - long callId = getCurrentCallId(); - if (callId != -1) { - // 在这里可以对获取到的通话记录ID进行处理 - //System.out.println("当前通话记录ID: " + callId); + // ====================== 内部枚举类(提前定义,便于业务调用) ====================== + public enum CallType { + CALL_IN, // 来电 + CALL_OUT // 去电 + } - // 电话接通,开始录音 - startRecording(callId); - } - break; - } - case TelephonyManager.CALL_STATE_IDLE: - // 电话挂断,停止录音 - stopRecording(); - break; - case Call.STATE_ACTIVE: { - break; - } - - case Call.STATE_DISCONNECTED: { - ActivityStack.getInstance().finishActivity(PhoneCallActivity.class); - break; - } - - } - } - }; + // ====================== Service生命周期方法区(按执行顺序排列) ====================== + @Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 ====="); + // 初始化通话状态回调(提前初始化,避免重复创建) + initCallCallback(); + LogUtils.d(TAG, "===== onCreate: 服务初始化完成 ====="); + } @Override public void onCallAdded(Call call) { super.onCallAdded(call); + LogUtils.d(TAG, "onCallAdded: 检测到新通话,开始处理"); - call.registerCallback(callback); - PhoneCallManager.call = call; - CallType callType = null; - - if (call.getState() == Call.STATE_RINGING) { - callType = CallType.CALL_IN; - } else if (call.getState() == Call.STATE_CONNECTING) { - callType = CallType.CALL_OUT; + // 空指针防护:避免通话对象为空导致崩溃 + if (call == null) { + LogUtils.e(TAG, "onCallAdded: 通话对象为空,跳过处理"); + return; } + // 注册通话状态回调 + call.registerCallback(mCallCallback); + PhoneCallManager.call = call; + LogUtils.d(TAG, "onCallAdded: 已注册通话回调,通话对象绑定完成"); + + // 判断通话类型(来电/去电) + CallType callType = judgeCallType(call); if (callType != null) { - Call.Details details = call.getDetails(); - String phoneNumber = details.getHandle().getSchemeSpecificPart(); - - // 记录原始铃声音量 - // - AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); - int ringerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); - // 恢复铃声音量,预防其他意外条件导致的音量变化问题 - // - - // 读取应用配置,未配置就初始化配置文件 - RingTongBean bean = RingTongBean.loadBean(this, RingTongBean.class); - if (bean == null) { - // 初始化配置 - bean = new RingTongBean(); - RingTongBean.saveBean(this, bean); - } - // 如果当前音量和应用保存的不一致就恢复为应用设定值 - // 恢复铃声音量 - try { - if (ringerVolume != bean.getStreamVolume()) { - audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0); - //audioManager.setMode(AudioManager.RINGER_MODE_NORMAL); - } - } catch (java.lang.SecurityException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - - // 检查电话接收规则 - if (!Rules.getInstance(this).isAllowed(phoneNumber)) { - // 调低音量 - try { - audioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0); - //audioManager.setMode(AudioManager.RINGER_MODE_SILENT); - } catch (java.lang.SecurityException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - // 断开电话 - call.disconnect(); - // 停顿1秒,预防第一声铃声响动 - try { - Thread.sleep(500); - } catch (InterruptedException e) { - LogUtils.d(TAG, ""); - } - // 恢复铃声音量 - try { - audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0); - //audioManager.setMode(AudioManager.RINGER_MODE_NORMAL); - } catch (java.lang.SecurityException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - // 屏蔽电话结束 - return; - } - - // 正常接听电话 - PhoneCallActivity.actionStart(this, phoneNumber, callType); + // 处理有效通话(音量控制、规则校验、启动通话界面) + handleValidCall(call, callType); + } else { + LogUtils.w(TAG, "onCallAdded: 无法识别通话类型,通话状态=" + call.getState()); } } @Override public void onCallRemoved(Call call) { super.onCallRemoved(call); - call.unregisterCallback(callback); + LogUtils.d(TAG, "onCallRemoved: 通话结束,开始清理资源"); + + // 空指针防护:避免通话对象为空导致崩溃 + if (call != null) { + call.unregisterCallback(mCallCallback); + LogUtils.d(TAG, "onCallRemoved: 已注销通话回调"); + } + PhoneCallManager.call = null; + LogUtils.d(TAG, "onCallRemoved: 通话资源清理完成"); } @Override public void onDestroy() { super.onDestroy(); + LogUtils.d(TAG, "onDestroy: 通话监听服务开始销毁"); + // 更新通话记录列表 CallLogFragment.updateCallLogFragment(); + LogUtils.d(TAG, "onDestroy: 通话监听服务销毁完成"); } - public enum CallType { - CALL_IN, - CALL_OUT, - } + // ====================== 核心初始化方法区 ====================== + /** + * 初始化通话状态回调,统一管理通话状态变更逻辑 + */ + private void initCallCallback() { + mCallCallback = new Call.Callback() { + @Override + public void onStateChanged(Call call, int state) { + super.onStateChanged(call, state); + LogUtils.d(TAG, "onStateChanged: 通话状态变更,状态码=" + state + ",状态描述=" + getCallStateDesc(state)); - - private void startRecording(long callId) { - LogUtils.d(TAG, "startRecording(...)"); - mediaRecorder = new MediaRecorder(); - mediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL); - mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mediaRecorder.setOutputFile(getOutputFilePath(callId)); - try { - mediaRecorder.prepare(); - mediaRecorder.start(); - } catch (IOException e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); - } - } - - private String getOutputFilePath(long callId) { - LogUtils.d(TAG, "getOutputFilePath(...)"); - // 设置录音文件的保存路径 - File file = new File(getExternalFilesDir(TAG), String.format("call_%d.mp4", callId)); - return file.getAbsolutePath(); - } - - private void stopRecording() { - LogUtils.d(TAG, "stopRecording()"); - if (mediaRecorder != null) { - mediaRecorder.stop(); - mediaRecorder.release(); - mediaRecorder = null; - } - } - - private long getCurrentCallId() { - LogUtils.d(TAG, "getCurrentCallId()"); - ContentResolver contentResolver = getApplicationContext().getContentResolver(); - Uri callLogUri = Uri.parse("content://call_log/calls"); - String[] projection = {"_id", "number", "call_type", "date"}; - String selection = "call_type = " + CallLog.Calls.OUTGOING_TYPE + " OR call_type = " + CallLog.Calls.INCOMING_TYPE; - String sortOrder = "date DESC"; - - try { - Cursor cursor = contentResolver.query(callLogUri, projection, selection, null, sortOrder); - if (cursor != null && cursor.moveToFirst()) { - return cursor.getLong(cursor.getColumnIndex("_id")); + switch (state) { + case TelephonyManager.CALL_STATE_IDLE: + // 通话空闲(挂断后),无需额外处理(原录音停止逻辑已删除) + LogUtils.d(TAG, "onStateChanged: 通话进入空闲状态"); + break; + case Call.STATE_DISCONNECTED: + // 通话断开,关闭通话界面 + ActivityStack.getInstance().finishActivity(PhoneCallActivity.class); + LogUtils.d(TAG, "onStateChanged: 通话断开,已关闭通话界面"); + break; + // 保留其他状态分支,便于后续扩展,无冗余逻辑 + case Call.STATE_ACTIVE: + LogUtils.d(TAG, "onStateChanged: 通话进入活跃状态"); + break; + default: + break; + } } - } catch (Exception e) { - LogUtils.d(TAG, e, Thread.currentThread().getStackTrace()); + }; + LogUtils.d(TAG, "initCallCallback: 通话状态回调初始化完成"); + } + + // ====================== 核心业务处理方法区 ====================== + /** + * 判断通话类型(来电/去电) + * @param call 通话对象 + * @return 通话类型枚举,无法识别返回null + */ + private CallType judgeCallType(Call call) { + int callState = call.getState(); + if (callState == Call.STATE_RINGING) { + LogUtils.d(TAG, "judgeCallType: 通话状态为响铃,识别为来电"); + return CallType.CALL_IN; + } else if (callState == Call.STATE_CONNECTING) { + LogUtils.d(TAG, "judgeCallType: 通话状态为连接中,识别为去电"); + return CallType.CALL_OUT; + } + return null; + } + + /** + * 处理有效通话(音量控制、拦截规则校验、启动通话界面) + * @param call 通话对象 + * @param callType 通话类型(来电/去电) + */ + private void handleValidCall(Call call, CallType callType) { + // 1. 获取通话详情与号码(多层空指针防护) + Call.Details callDetails = call.getDetails(); + if (callDetails == null || callDetails.getHandle() == null) { + LogUtils.e(TAG, "handleValidCall: 通话详情或号码信息为空,跳过后续处理"); + return; + } + String phoneNumber = callDetails.getHandle().getSchemeSpecificPart(); + LogUtils.d(TAG, "handleValidCall: 开始处理通话,号码=" + phoneNumber + ",类型=" + callType); + + // 2. 初始化音频管理器(音量控制核心) + AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); + if (audioManager == null) { + LogUtils.e(TAG, "handleValidCall: 获取音频管理器失败,无法处理音量控制"); + // 音量控制失败仍启动通话界面,保障基础功能可用 + PhoneCallActivity.actionStart(this, phoneNumber, callType); + return; } - return -1; + // 3. 处理铃声音量(恢复配置音量、拦截时静音) + handleRingerVolumeControl(audioManager, phoneNumber, call); + + // 4. 校验通过,启动通话界面(拦截场景已提前返回,此处直接启动) + PhoneCallActivity.actionStart(this, phoneNumber, callType); + LogUtils.d(TAG, "handleValidCall: 通话校验通过,已启动通话界面"); + } + + /** + * 铃声音量控制(恢复应用配置音量、拦截号码静音处理) + * @param audioManager 音频管理器 + * @param phoneNumber 通话号码 + * @param call 通话对象(用于拦截时断开通话) + */ + private void handleRingerVolumeControl(AudioManager audioManager, String phoneNumber, Call call) { + // 3.1 获取当前铃声音量 + int currentRingerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); + LogUtils.d(TAG, "handleRingerVolumeControl: 当前铃声音量=" + currentRingerVolume); + + // 3.2 加载/初始化铃声音量配置 + RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class); + if (ringTongBean == null) { + ringTongBean = new RingTongBean(); + RingTongBean.saveBean(this, ringTongBean); + LogUtils.d(TAG, "handleRingerVolumeControl: 铃声音量配置未初始化,已自动创建默认配置"); + } + int configRingerVolume = ringTongBean.getStreamVolume(); + LogUtils.d(TAG, "handleRingerVolumeControl: 应用配置铃声音量=" + configRingerVolume); + + // 3.3 恢复应用配置音量(当前音量与配置不一致时调整) + try { + if (currentRingerVolume != configRingerVolume) { + audioManager.setStreamVolume(AudioManager.STREAM_RING, configRingerVolume, 0); + LogUtils.d(TAG, "handleRingerVolumeControl: 已将铃声音量恢复为应用配置值"); + } else { + LogUtils.d(TAG, "handleRingerVolumeControl: 当前音量与配置一致,无需调整"); + } + } catch (SecurityException e) { + LogUtils.e(TAG, "handleRingerVolumeControl: 恢复铃声音量失败,权限不足", e); + return; + } + + // 3.4 校验拦截规则,拦截号码静音+断开通话 + if (!Rules.getInstance(this).isAllowed(phoneNumber)) { + LogUtils.d(TAG, "handleRingerVolumeControl: 号码=" + phoneNumber + " 命中拦截规则,开始拦截处理"); + try { + // 静音处理 + audioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0); + LogUtils.d(TAG, "handleRingerVolumeControl: 已将铃声音量设为0(静音)"); + } catch (SecurityException e) { + LogUtils.e(TAG, "handleRingerVolumeControl: 拦截静音失败,权限不足", e); + } + + // 断开通话 + call.disconnect(); + LogUtils.d(TAG, "handleRingerVolumeControl: 已断开拦截通话"); + + // 延迟恢复音量(防止第一声铃声响动) + try { + Thread.sleep(500); + audioManager.setStreamVolume(AudioManager.STREAM_RING, configRingerVolume, 0); + LogUtils.d(TAG, "handleRingerVolumeControl: 延迟500ms后,已恢复铃声音量为配置值"); + } catch (InterruptedException e) { + LogUtils.e(TAG, "handleRingerVolumeControl: 延迟恢复音量失败,线程被中断", e); + } catch (SecurityException e) { + LogUtils.e(TAG, "handleRingerVolumeControl: 恢复音量失败,权限不足", e); + } + + // 拦截完成,直接返回,不启动通话界面 + LogUtils.d(TAG, "handleRingerVolumeControl: 拦截处理完成,跳过通话界面启动"); + return; + } + + LogUtils.d(TAG, "handleRingerVolumeControl: 号码=" + phoneNumber + " 未命中拦截规则,音量控制完成"); + } + + // ====================== 辅助工具方法区 ====================== + /** + * 通话状态码转文字描述(便于日志查看,快速定位状态) + * @param state 通话状态码(TelephonyManager/Call 中的常量) + * @return 状态文字描述 + */ + private String getCallStateDesc(int state) { + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + return "响铃中"; + case TelephonyManager.CALL_STATE_OFFHOOK: + return "通话中"; + case TelephonyManager.CALL_STATE_IDLE: + return "空闲(未通话/已挂断)"; + case Call.STATE_ACTIVE: + return "通话活跃"; + case Call.STATE_CONNECTING: + return "通话连接中"; + case Call.STATE_DISCONNECTED: + return "通话已断开"; + default: + return "未知状态"; + } } }