恢复2a74fd2c304b571ab5ae349ffc3b7f06c5b4daf7提交点历史
This commit is contained in:
@@ -8,28 +8,25 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.contacts.ActivityStack;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 接打电话界面,仅支持 Android 6.0(API 23)及以上系统
|
||||
*/
|
||||
// Java7不支持@RequiresApi,通过代码内校验替代注解
|
||||
public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneCallActivity";
|
||||
// Intent传参键常量,替代系统常量避免歧义
|
||||
private static final String EXTRA_CALL_TYPE = "extra_call_type";
|
||||
private static final String EXTRA_PHONE_NUM = "extra_phone_number";
|
||||
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
||||
|
||||
|
||||
/**
|
||||
* 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
|
||||
*
|
||||
* @author aJIEw
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
|
||||
// ====================== 控件与成员变量区 ======================
|
||||
private TextView tvCallNumberLabel;
|
||||
private TextView tvCallNumber;
|
||||
private TextView tvPickUp;
|
||||
@@ -39,254 +36,126 @@ public class PhoneCallActivity extends AppCompatActivity implements View.OnClick
|
||||
private PhoneCallManager phoneCallManager;
|
||||
private PhoneCallService.CallType callType;
|
||||
private String phoneNumber;
|
||||
|
||||
private Timer onGoingCallTimer;
|
||||
private int callingTime;
|
||||
|
||||
// ====================== 静态跳转方法区 ======================
|
||||
/**
|
||||
* 启动通话界面
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 通话号码
|
||||
* @param callType 通话类型(来电/去电)
|
||||
*/
|
||||
public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "actionStart: 上下文为null,无法启动通话界面");
|
||||
return;
|
||||
}
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "actionStart: 通话号码为空");
|
||||
}
|
||||
if (callType == null) {
|
||||
LogUtils.w(TAG, "actionStart: 通话类型为null");
|
||||
}
|
||||
|
||||
public static void actionStart(Context context, String phoneNumber,
|
||||
PhoneCallService.CallType callType) {
|
||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(EXTRA_CALL_TYPE, callType);
|
||||
intent.putExtra(EXTRA_PHONE_NUM, phoneNumber);
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
|
||||
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
||||
context.startActivity(intent);
|
||||
LogUtils.d(TAG, "actionStart: 启动通话界面,号码=" + phoneNumber + ",类型=" + callType);
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 通话界面创建");
|
||||
|
||||
// Java7不支持@RequiresApi,此处补充系统版本校验
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.e(TAG, "onCreate: 系统版本低于Android 6.0,不支持该通话界面");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.activity_phone_call);
|
||||
|
||||
ActivityStack.getInstance().addActivity(this);
|
||||
initData();
|
||||
initView();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 通话界面销毁");
|
||||
stopTimer();
|
||||
if (phoneCallManager != null) {
|
||||
phoneCallManager.destroy();
|
||||
}
|
||||
ActivityStack.getInstance().removeActivity(this);
|
||||
}
|
||||
|
||||
// ====================== 初始化方法区 ======================
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
private void initData() {
|
||||
phoneCallManager = new PhoneCallManager(this);
|
||||
onGoingCallTimer = new Timer();
|
||||
parseIntentData();
|
||||
LogUtils.d(TAG, "initData: 数据初始化完成,通话类型=" + callType + ",号码=" + phoneNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Intent传递的数据
|
||||
*/
|
||||
private void parseIntentData() {
|
||||
Intent intent = getIntent();
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "parseIntentData: 未获取到启动Intent");
|
||||
return;
|
||||
if (getIntent() != null) {
|
||||
phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||
callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES);
|
||||
}
|
||||
phoneNumber = intent.getStringExtra(EXTRA_PHONE_NUM);
|
||||
callType = (PhoneCallService.CallType) intent.getSerializableExtra(EXTRA_CALL_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化界面控件
|
||||
*/
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void initView() {
|
||||
// 设置沉浸式导航栏
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION //hide navigationBar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
LogUtils.d(TAG, "initView: 设置界面沉浸式样式");
|
||||
|
||||
// 绑定控件
|
||||
tvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
||||
tvCallNumber = findViewById(R.id.tv_call_number);
|
||||
tvPickUp = findViewById(R.id.tv_phone_pick_up);
|
||||
tvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
||||
tvHangUp = findViewById(R.id.tv_phone_hang_up);
|
||||
|
||||
// 设置控件事件与初始状态
|
||||
tvCallNumber.setText(CallListenerService.formatPhoneNumber(phoneNumber));
|
||||
tvCallNumber.setText(formatPhoneNumber(phoneNumber));
|
||||
tvPickUp.setOnClickListener(this);
|
||||
tvHangUp.setOnClickListener(this);
|
||||
|
||||
setCallTypeUi();
|
||||
showOnLockScreen();
|
||||
LogUtils.d(TAG, "initView: 界面控件初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 界面交互方法区 ======================
|
||||
/**
|
||||
* 根据通话类型设置界面样式
|
||||
*/
|
||||
private void setCallTypeUi() {
|
||||
if (callType == null) {
|
||||
LogUtils.w(TAG, "setCallTypeUi: 通话类型为null,使用默认样式");
|
||||
tvCallNumberLabel.setText("通话号码");
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PhoneCallService.CallType.CALL_IN == callType) {
|
||||
// 打进的电话
|
||||
if (callType == PhoneCallService.CallType.CALL_IN) {
|
||||
tvCallNumberLabel.setText("来电号码");
|
||||
tvPickUp.setVisibility(View.VISIBLE);
|
||||
} else if (PhoneCallService.CallType.CALL_OUT == callType) {
|
||||
} else if (callType == PhoneCallService.CallType.CALL_OUT) {
|
||||
tvCallNumberLabel.setText("呼叫号码");
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
if (phoneCallManager != null) {
|
||||
phoneCallManager.openSpeaker();
|
||||
LogUtils.d(TAG, "setCallTypeUi: 去电模式,开启扬声器");
|
||||
}
|
||||
phoneCallManager.openSpeaker();
|
||||
}
|
||||
|
||||
showOnLockScreen();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置界面锁屏显示
|
||||
*/
|
||||
private void showOnLockScreen() {
|
||||
getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
);
|
||||
LogUtils.d(TAG, "showOnLockScreen: 设置界面支持锁屏显示");
|
||||
public void showOnLockScreen() {
|
||||
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN |
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||
}
|
||||
|
||||
// ====================== 事件点击与计时方法区 ======================
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == null) {
|
||||
LogUtils.w(TAG, "onClick: 点击空控件");
|
||||
return;
|
||||
}
|
||||
|
||||
int id = v.getId();
|
||||
if (id == R.id.tv_phone_pick_up) {
|
||||
LogUtils.d(TAG, "onClick: 点击接听按钮");
|
||||
answerCall();
|
||||
} else if (id == R.id.tv_phone_hang_up) {
|
||||
LogUtils.d(TAG, "onClick: 点击挂断按钮");
|
||||
hangUpCall();
|
||||
} else {
|
||||
LogUtils.w(TAG, "onClick: 点击未知控件,ID=" + id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接听通话
|
||||
*/
|
||||
private void answerCall() {
|
||||
if (phoneCallManager != null) {
|
||||
if (v.getId() == R.id.tv_phone_pick_up) {
|
||||
phoneCallManager.answer();
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
tvCallingTime.setVisibility(View.VISIBLE);
|
||||
startCallTimer();
|
||||
LogUtils.d(TAG, "answerCall: 接听通话成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "answerCall: 通话管理器为空,无法接听");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂断通话
|
||||
*/
|
||||
private void hangUpCall() {
|
||||
if (phoneCallManager != null) {
|
||||
onGoingCallTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void run() {
|
||||
callingTime++;
|
||||
tvCallingTime.setText("通话中:" + getCallingTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, 1000);
|
||||
} else if (v.getId() == R.id.tv_phone_hang_up) {
|
||||
phoneCallManager.disconnect();
|
||||
} else {
|
||||
LogUtils.e(TAG, "hangUpCall: 通话管理器为空,无法挂断");
|
||||
stopTimer();
|
||||
}
|
||||
stopTimer();
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动通话计时器
|
||||
*/
|
||||
private void startCallTimer() {
|
||||
stopTimer();
|
||||
onGoingCallTimer = new Timer();
|
||||
onGoingCallTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void run() {
|
||||
callingTime++;
|
||||
tvCallingTime.setText("通话中:" + getCallingTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0, 1000);
|
||||
LogUtils.d(TAG, "startCallTimer: 通话计时器启动");
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止通话计时器
|
||||
*/
|
||||
private void stopTimer() {
|
||||
if (onGoingCallTimer != null) {
|
||||
onGoingCallTimer.cancel();
|
||||
onGoingCallTimer = null;
|
||||
}
|
||||
callingTime = 0;
|
||||
LogUtils.d(TAG, "stopTimer: 通话计时器停止");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化通话时长
|
||||
*/
|
||||
private String getCallingTime() {
|
||||
int minute = callingTime / 60;
|
||||
int second = callingTime % 60;
|
||||
String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute);
|
||||
String secondStr = second < 10 ? "0" + second : String.valueOf(second);
|
||||
return minuteStr + ":" + secondStr;
|
||||
return (minute < 10 ? "0" + minute : minute) +
|
||||
":" +
|
||||
(second < 10 ? "0" + second : second);
|
||||
}
|
||||
|
||||
private void stopTimer() {
|
||||
if (onGoingCallTimer != null) {
|
||||
onGoingCallTimer.cancel();
|
||||
}
|
||||
|
||||
callingTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//MainActivity.updateCallLogFragment();
|
||||
phoneCallManager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,139 +5,58 @@ import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.VideoProfile;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 通话管理工具类,负责接听、挂断通话及免提控制,仅支持Android 6.0(API 23)及以上系统
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallManager {
|
||||
// ====================== 常量定义区 ======================
|
||||
private static final String TAG = "PhoneCallManager";
|
||||
// 音频通话模式常量,与VideoProfile.STATE_AUDIO_ONLY保持一致,增强可读性
|
||||
private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 静态通话实例,用于全局关联当前通话
|
||||
public static Call sCurrentCall;
|
||||
// 上下文对象
|
||||
private Context mContext;
|
||||
// 音频管理器,用于控制免提等音频相关操作
|
||||
private AudioManager mAudioManager;
|
||||
public static Call call;
|
||||
|
||||
private Context context;
|
||||
private AudioManager audioManager;
|
||||
|
||||
// ====================== 构造方法区 ======================
|
||||
/**
|
||||
* 构造通话管理实例
|
||||
* @param context 上下文
|
||||
*/
|
||||
public PhoneCallManager(Context context) {
|
||||
this.mContext = context;
|
||||
initAudioManager();
|
||||
LogUtils.d(TAG, "PhoneCallManager: 通话管理工具初始化");
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
// ====================== 初始化辅助方法区 ======================
|
||||
/**
|
||||
* 初始化音频管理器
|
||||
*/
|
||||
private void initAudioManager() {
|
||||
if (mContext == null) {
|
||||
LogUtils.e(TAG, "initAudioManager: 上下文为空,无法初始化音频管理器");
|
||||
return;
|
||||
}
|
||||
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.e(TAG, "initAudioManager: 获取音频管理器失败");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initAudioManager: 音频管理器初始化成功");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 通话核心操作方法区 ======================
|
||||
/**
|
||||
* 接听当前通话
|
||||
* 接听电话
|
||||
*/
|
||||
public void answer() {
|
||||
// 校验系统版本,Android6.0以下不支持telecom.Call的answer方法
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.e(TAG, "answer: 系统版本低于Android6.0,不支持接听操作");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sCurrentCall == null) {
|
||||
LogUtils.w(TAG, "answer: 当前无通话实例,无法接听");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
sCurrentCall.answer(VIDEO_PROFILE_AUDIO_ONLY);
|
||||
if (call != null) {
|
||||
call.answer(VideoProfile.STATE_AUDIO_ONLY);
|
||||
openSpeaker();
|
||||
LogUtils.d(TAG, "answer: 成功执行接听操作,通话模式为纯音频");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "answer: 接听通话权限不足", e);
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, "answer: 通话状态异常,无法接听", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开当前通话(拒接或挂断)
|
||||
* 断开电话,包括来电时的拒接以及接听后的挂断
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
LogUtils.e(TAG, "disconnect: 系统版本低于Android6.0,不支持挂断操作");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sCurrentCall == null) {
|
||||
LogUtils.w(TAG, "disconnect: 当前无通话实例,无法挂断");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
sCurrentCall.disconnect();
|
||||
LogUtils.d(TAG, "disconnect: 成功执行挂断操作");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "disconnect: 挂断通话权限不足", e);
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, "disconnect: 通话状态异常,无法挂断", e);
|
||||
if (call != null) {
|
||||
call.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开通话免提
|
||||
* 打开免提
|
||||
*/
|
||||
public void openSpeaker() {
|
||||
if (mAudioManager == null) {
|
||||
LogUtils.w(TAG, "openSpeaker: 音频管理器为空,无法打开免提");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
mAudioManager.setSpeakerphoneOn(true);
|
||||
LogUtils.d(TAG, "openSpeaker: 免提功能已开启");
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, "openSpeaker: 音频模式设置失败,无法开启免提", e);
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 资源释放方法区 ======================
|
||||
/**
|
||||
* 销毁资源,释放引用
|
||||
* 销毁资源
|
||||
*/
|
||||
public void destroy() {
|
||||
// 关闭免提后再释放资源,避免音频状态异常
|
||||
if (mAudioManager != null) {
|
||||
mAudioManager.setSpeakerphoneOn(false);
|
||||
mAudioManager.setMode(AudioManager.MODE_NORMAL);
|
||||
LogUtils.d(TAG, "destroy: 音频状态已恢复默认");
|
||||
}
|
||||
sCurrentCall = null;
|
||||
mAudioManager = null;
|
||||
mContext = null;
|
||||
LogUtils.d(TAG, "destroy: 通话管理工具资源已释放");
|
||||
call = null;
|
||||
context = null;
|
||||
audioManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,243 +1,216 @@
|
||||
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;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.ActivityStack;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 通话状态监听服务(需 Android 6.0+),负责通话状态回调、铃音控制及黑白名单校验
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class PhoneCallService extends InCallService {
|
||||
// ====================== 常量定义区 ======================
|
||||
|
||||
public static final String TAG = "PhoneCallService";
|
||||
// Call 通话状态(固定正确值,避免冲突)
|
||||
private static final int CALL_STATE_IDLE = Call.STATE_NEW;
|
||||
private static final int CALL_STATE_RINGING = Call.STATE_RINGING; // 正确值=1
|
||||
private static final int CALL_STATE_CONNECTING = Call.STATE_CONNECTING; // 正确值=3
|
||||
private static final int CALL_STATE_ACTIVE = Call.STATE_ACTIVE; // 正确值=2
|
||||
private static final int CALL_STATE_DISCONNECTED = Call.STATE_DISCONNECTED; // 正确值=4
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Call.Callback mCallCallback;
|
||||
MediaRecorder mediaRecorder;
|
||||
|
||||
// ====================== 内部枚举类 ======================
|
||||
public enum CallType {
|
||||
CALL_IN,
|
||||
CALL_OUT
|
||||
}
|
||||
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);
|
||||
|
||||
// ====================== 生命周期方法区 ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
initCallCallback();
|
||||
LogUtils.d(TAG, "onCreate: 通话监听服务已创建");
|
||||
}
|
||||
// 电话接通,开始录音
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCallAdded(Call call) {
|
||||
super.onCallAdded(call);
|
||||
if (call == null) {
|
||||
LogUtils.w(TAG, "onCallAdded: 新增通话为null,跳过处理");
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
call.registerCallback(mCallCallback);
|
||||
PhoneCallManager.sCurrentCall = call;
|
||||
LogUtils.d(TAG, "onCallAdded: 新增通话已注册回调 | 状态码=" + call.getState() + " | 描述=" + getCallStateDesc(call.getState()));
|
||||
if (callType != null) {
|
||||
Call.Details details = call.getDetails();
|
||||
String phoneNumber = details.getHandle().getSchemeSpecificPart();
|
||||
|
||||
CallType callType = judgeCallType(call.getState());
|
||||
if (callType == null) {
|
||||
LogUtils.w(TAG, "onCallAdded: 无法识别通话类型 | 状态码=" + call.getState());
|
||||
return;
|
||||
// 记录原始铃声音量
|
||||
//
|
||||
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);
|
||||
}
|
||||
|
||||
String phoneNumber = getPhoneNumberFromCall(call);
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "onCallAdded: 通话号码为空");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "onCallAdded: 通话类型=" + callType.name() + " | 号码=" + phoneNumber);
|
||||
|
||||
handleRingerAndRuleCheck(phoneNumber, callType, call);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallRemoved(Call call) {
|
||||
super.onCallRemoved(call);
|
||||
if (call != null) {
|
||||
call.unregisterCallback(mCallCallback);
|
||||
LogUtils.d(TAG, "onCallRemoved: 通话回调已注销");
|
||||
}
|
||||
PhoneCallManager.sCurrentCall = null;
|
||||
call.unregisterCallback(callback);
|
||||
PhoneCallManager.call = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
CallLogFragment.updateCallLogFragment();
|
||||
LogUtils.d(TAG, "onDestroy: 通话服务已销毁,资源已释放");
|
||||
}
|
||||
|
||||
// ====================== 核心:通话状态回调 ======================
|
||||
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));
|
||||
|
||||
switch (state) {
|
||||
// 通话活跃状态(仅监听,无录音)
|
||||
case CALL_STATE_ACTIVE:
|
||||
LogUtils.d(TAG, "onStateChanged: 通话已接通/活跃");
|
||||
break;
|
||||
// 响铃状态
|
||||
case CALL_STATE_RINGING:
|
||||
LogUtils.d(TAG, "onStateChanged: 通话处于响铃状态");
|
||||
break;
|
||||
// 空闲状态(通话挂断后)
|
||||
case 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_CONNECTING:
|
||||
LogUtils.d(TAG, "onStateChanged: 通话正在连接");
|
||||
break;
|
||||
default:
|
||||
LogUtils.w(TAG, "onStateChanged: 未处理的状态码=" + state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
public enum CallType {
|
||||
CALL_IN,
|
||||
CALL_OUT,
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法区(铃音控制+黑白名单) ======================
|
||||
private CallType judgeCallType(int callState) {
|
||||
switch (callState) {
|
||||
case CALL_STATE_RINGING:
|
||||
return CallType.CALL_IN;
|
||||
case CALL_STATE_CONNECTING:
|
||||
return CallType.CALL_OUT;
|
||||
default:
|
||||
return null;
|
||||
|
||||
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 getPhoneNumberFromCall(Call call) {
|
||||
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 {
|
||||
Call.Details details = call.getDetails();
|
||||
if (details == null || details.getHandle() == null) {
|
||||
return null;
|
||||
Cursor cursor = contentResolver.query(callLogUri, projection, selection, null, sortOrder);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndex("_id"));
|
||||
}
|
||||
return details.getHandle().getSchemeSpecificPart();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getPhoneNumberFromCall: 解析号码失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRingerAndRuleCheck(String phoneNumber, CallType callType, Call call) {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "handleRingerAndRuleCheck: 音频管理器获取失败");
|
||||
startPhoneCallActivity(phoneNumber, callType);
|
||||
return;
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
|
||||
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (ringTongBean == null) {
|
||||
ringTongBean = new RingTongBean();
|
||||
RingTongBean.saveBean(this, ringTongBean);
|
||||
LogUtils.d(TAG, "handleRingerAndRuleCheck: 铃音配置已初始化");
|
||||
}
|
||||
|
||||
restoreRingerVolume(audioManager, ringTongBean);
|
||||
|
||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
||||
handleForbiddenCall(audioManager, ringTongBean, call);
|
||||
return;
|
||||
}
|
||||
|
||||
startPhoneCallActivity(phoneNumber, callType);
|
||||
}
|
||||
|
||||
private void restoreRingerVolume(AudioManager audioManager, RingTongBean bean) {
|
||||
try {
|
||||
int currentVol = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
int configVol = bean.getStreamVolume();
|
||||
if (currentVol != configVol) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, configVol, 0);
|
||||
LogUtils.d(TAG, "restoreRingerVolume: 音量恢复 | 配置值=" + configVol + " | 当前值=" + currentVol);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "restoreRingerVolume: 音量设置失败(权限不足)", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleForbiddenCall(AudioManager audioManager, RingTongBean bean, Call call) {
|
||||
try {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
||||
call.disconnect();
|
||||
Thread.sleep(500);
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, bean.getStreamVolume(), 0);
|
||||
LogUtils.d(TAG, "handleForbiddenCall: 禁止通话处理完成");
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "handleForbiddenCall: 音量操作失败", e);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "handleForbiddenCall: 延迟线程中断", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private void startPhoneCallActivity(String phoneNumber, CallType callType) {
|
||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
||||
LogUtils.d(TAG, "startPhoneCallActivity: 通话界面已启动");
|
||||
}
|
||||
|
||||
// ====================== 辅助工具方法区 ======================
|
||||
private String getCallStateDesc(int state) {
|
||||
switch (state) {
|
||||
case CALL_STATE_RINGING:
|
||||
return "响铃";
|
||||
case CALL_STATE_CONNECTING:
|
||||
return "连接中";
|
||||
case CALL_STATE_ACTIVE:
|
||||
return "活跃";
|
||||
case CALL_STATE_IDLE:
|
||||
return "空闲";
|
||||
case CALL_STATE_DISCONNECTED:
|
||||
return "已断开";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 静态内部类:PhoneCallManager(避免编译报错) ======================
|
||||
public static class PhoneCallManager {
|
||||
public static Call sCurrentCall;
|
||||
|
||||
private PhoneCallManager() {
|
||||
// 私有构造,禁止实例化
|
||||
}
|
||||
|
||||
public static Call getCurrentCall() {
|
||||
return sCurrentCall;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user