源码整理
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Fri Dec 12 05:20:34 GMT 2025
|
||||
#Fri Dec 12 06:22:08 GMT 2025
|
||||
stageCount=1
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.0
|
||||
buildCount=5
|
||||
buildCount=10
|
||||
baseBetaVersion=15.12.1
|
||||
|
||||
@@ -1,59 +1,139 @@
|
||||
package cc.winboll.studio.contacts;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe Activity 栈管理工具,用于统一管理应用内 Activity 生命周期
|
||||
*/
|
||||
public class ActivityStack {
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ActivityStack";
|
||||
|
||||
// ====================== 单例与成员变量区 ======================
|
||||
private static final ActivityStack INSTANCE = new ActivityStack();
|
||||
private List<Activity> mActivityList = new ArrayList<Activity>();
|
||||
|
||||
private List<Activity> activities = new ArrayList<>();
|
||||
|
||||
// ====================== 单例获取方法区 ======================
|
||||
public static ActivityStack getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void addActivity(Activity activity) {
|
||||
activities.add(activity);
|
||||
// ====================== 私有构造函数(防止外部实例化) ======================
|
||||
private ActivityStack() {
|
||||
LogUtils.d(TAG, "ActivityStack: 初始化 Activity 栈管理工具");
|
||||
}
|
||||
|
||||
// ====================== Activity 栈操作方法区 ======================
|
||||
/**
|
||||
* 添加 Activity 到栈中
|
||||
*/
|
||||
public void addActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "addActivity: 待添加的 Activity 为 null,跳过添加");
|
||||
return;
|
||||
}
|
||||
mActivityList.add(activity);
|
||||
LogUtils.d(TAG, "addActivity: Activity入栈 | 类名=" + activity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取栈顶 Activity
|
||||
*/
|
||||
public Activity getTopActivity() {
|
||||
if (activities.isEmpty()) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "getTopActivity: Activity 栈为空,返回 null");
|
||||
return null;
|
||||
}
|
||||
return activities.get(activities.size() - 1);
|
||||
Activity topActivity = mActivityList.get(mActivityList.size() - 1);
|
||||
LogUtils.d(TAG, "getTopActivity: 获取栈顶 Activity | 类名=" + topActivity.getClass().getSimpleName());
|
||||
return topActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除并销毁栈顶 Activity
|
||||
*/
|
||||
public void finishTopActivity() {
|
||||
if (!activities.isEmpty()) {
|
||||
activities.remove(activities.size() - 1).finish();
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishTopActivity: Activity 栈为空,无需操作");
|
||||
return;
|
||||
}
|
||||
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
|
||||
topActivity.finish();
|
||||
LogUtils.d(TAG, "finishTopActivity: 销毁栈顶 Activity | 类名=" + topActivity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除并销毁指定 Activity
|
||||
*/
|
||||
public void finishActivity(Activity activity) {
|
||||
if (activity != null) {
|
||||
activities.remove(activity);
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "finishActivity: 待销毁的 Activity 为 null,跳过操作");
|
||||
return;
|
||||
}
|
||||
if (mActivityList.remove(activity)) {
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishActivity: 销毁指定 Activity | 类名=" + activity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
||||
} else {
|
||||
LogUtils.w(TAG, "finishActivity: 指定 Activity 不在栈中 | 类名=" + activity.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public void finishActivity(Class activityClass) {
|
||||
for (Activity activity : activities) {
|
||||
/**
|
||||
* 移除并销毁指定类的所有 Activity
|
||||
*/
|
||||
public void finishActivity(Class<?> activityClass) {
|
||||
if (activityClass == null) {
|
||||
LogUtils.w(TAG, "finishActivity: 待销毁的 Activity 类为 null,跳过操作");
|
||||
return;
|
||||
}
|
||||
// Java7 兼容:使用 Iterator 遍历避免 ConcurrentModificationException
|
||||
Iterator<Activity> iterator = mActivityList.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Activity activity = iterator.next();
|
||||
if (activity.getClass().equals(activityClass)) {
|
||||
finishActivity(activity);
|
||||
iterator.remove();
|
||||
activity.finish();
|
||||
LogUtils.d(TAG, "finishActivity: 销毁指定类 Activity | 类名=" + activityClass.getSimpleName() + " | 栈大小=" + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁栈中所有 Activity
|
||||
*/
|
||||
public void finishAllActivity() {
|
||||
if (!activities.isEmpty()) {
|
||||
for (Activity activity : activities) {
|
||||
if (mActivityList.isEmpty()) {
|
||||
LogUtils.w(TAG, "finishAllActivity: Activity 栈为空,无需操作");
|
||||
return;
|
||||
}
|
||||
// Java7 兼容:使用增强 for 循环遍历销毁,避免迭代器异常
|
||||
for (Activity activity : mActivityList) {
|
||||
if (!activity.isFinishing()) {
|
||||
activity.finish();
|
||||
activities.remove(activity);
|
||||
LogUtils.d(TAG, "finishAllActivity: 销毁 Activity | 类名=" + activity.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
mActivityList.clear();
|
||||
LogUtils.d(TAG, "finishAllActivity: 所有 Activity 已销毁,栈已清空");
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增:移除指定Activity但不销毁(用于Activity正常退出)
|
||||
*/
|
||||
public void removeActivity(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.w(TAG, "removeActivity: 待移除的 Activity 为 null,跳过操作");
|
||||
return;
|
||||
}
|
||||
if (mActivityList.remove(activity)) {
|
||||
LogUtils.d(TAG, "removeActivity: 移除 Activity | 类名=" + activity.getClass().getSimpleName() + " | 栈大小=" + mActivityList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ public final class MainActivity extends AppCompatActivity implements IWinBoLLAct
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户点击设置权限,跳转应用设置页");
|
||||
AppGoToSettingsUtil appGoToSettingsUtil = new AppGoToSettingsUtil();
|
||||
appGoToSettingsUtil.GoToSetting(MainActivity.this);
|
||||
appGoToSettingsUtil.goToSetting(MainActivity.this);
|
||||
}
|
||||
})
|
||||
.setPositiveButton("确定退出", new android.content.DialogInterface.OnClickListener() {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV
|
||||
*/
|
||||
public void relaodContacts() {
|
||||
LogUtils.d(TAG, "relaodContacts: 开始重新加载联系人数据");
|
||||
this.mContactUtils.relaodContacts();
|
||||
this.mContactUtils.reloadContacts();
|
||||
notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "relaodContacts: 联系人数据加载完成,列表已刷新");
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogV
|
||||
final CallLogModel callLog = callLogList.get(position);
|
||||
|
||||
// 绑定通话号码与联系人名称
|
||||
String contactName = mContactUtils.getContactsName(callLog.getPhoneNumber());
|
||||
String contactName = mContactUtils.getContactName(callLog.getPhoneNumber());
|
||||
String phoneText = callLog.getPhoneNumber() + "☎" + (contactName == null ? "" : contactName);
|
||||
holder.phoneNumber.setText(phoneText);
|
||||
|
||||
|
||||
@@ -8,25 +8,28 @@ 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;
|
||||
|
||||
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
||||
|
||||
|
||||
/**
|
||||
* 提供接打电话的界面,仅支持 Android M (6.0, API 23) 及以上的系统
|
||||
*
|
||||
* @author aJIEw
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 接打电话界面,仅支持 Android 6.0(API 23)及以上系统
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
// 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";
|
||||
|
||||
// ====================== 控件与成员变量区 ======================
|
||||
private TextView tvCallNumberLabel;
|
||||
private TextView tvCallNumber;
|
||||
private TextView tvPickUp;
|
||||
@@ -36,126 +39,254 @@ public class PhoneCallActivity extends AppCompatActivity implements View.OnClick
|
||||
private PhoneCallManager phoneCallManager;
|
||||
private PhoneCallService.CallType callType;
|
||||
private String phoneNumber;
|
||||
|
||||
private Timer onGoingCallTimer;
|
||||
private int callingTime;
|
||||
|
||||
public static void actionStart(Context context, String phoneNumber,
|
||||
PhoneCallService.CallType callType) {
|
||||
// ====================== 静态跳转方法区 ======================
|
||||
/**
|
||||
* 启动通话界面
|
||||
* @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");
|
||||
}
|
||||
|
||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, callType);
|
||||
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
||||
intent.putExtra(EXTRA_CALL_TYPE, callType);
|
||||
intent.putExtra(EXTRA_PHONE_NUM, phoneNumber);
|
||||
context.startActivity(intent);
|
||||
LogUtils.d(TAG, "actionStart: 启动通话界面,号码=" + phoneNumber + ",类型=" + callType);
|
||||
}
|
||||
|
||||
// ====================== 生命周期方法区 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_phone_call);
|
||||
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();
|
||||
if (getIntent() != null) {
|
||||
phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
||||
callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES);
|
||||
}
|
||||
parseIntentData();
|
||||
LogUtils.d(TAG, "initData: 数据初始化完成,通话类型=" + callType + ",号码=" + phoneNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Intent传递的数据
|
||||
*/
|
||||
private void parseIntentData() {
|
||||
Intent intent = getIntent();
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "parseIntentData: 未获取到启动Intent");
|
||||
return;
|
||||
}
|
||||
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 //hide navigationBar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| 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(formatPhoneNumber(phoneNumber));
|
||||
// 设置控件事件与初始状态
|
||||
tvCallNumber.setText(CallListenerService.formatPhoneNumber(phoneNumber));
|
||||
tvPickUp.setOnClickListener(this);
|
||||
tvHangUp.setOnClickListener(this);
|
||||
|
||||
// 打进的电话
|
||||
if (callType == PhoneCallService.CallType.CALL_IN) {
|
||||
tvCallNumberLabel.setText("来电号码");
|
||||
tvPickUp.setVisibility(View.VISIBLE);
|
||||
} else if (callType == PhoneCallService.CallType.CALL_OUT) {
|
||||
tvCallNumberLabel.setText("呼叫号码");
|
||||
setCallTypeUi();
|
||||
showOnLockScreen();
|
||||
LogUtils.d(TAG, "initView: 界面控件初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 界面交互方法区 ======================
|
||||
/**
|
||||
* 根据通话类型设置界面样式
|
||||
*/
|
||||
private void setCallTypeUi() {
|
||||
if (callType == null) {
|
||||
LogUtils.w(TAG, "setCallTypeUi: 通话类型为null,使用默认样式");
|
||||
tvCallNumberLabel.setText("通话号码");
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
phoneCallManager.openSpeaker();
|
||||
return;
|
||||
}
|
||||
|
||||
showOnLockScreen();
|
||||
if (PhoneCallService.CallType.CALL_IN == callType) {
|
||||
tvCallNumberLabel.setText("来电号码");
|
||||
tvPickUp.setVisibility(View.VISIBLE);
|
||||
} else if (PhoneCallService.CallType.CALL_OUT == callType) {
|
||||
tvCallNumberLabel.setText("呼叫号码");
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
if (phoneCallManager != null) {
|
||||
phoneCallManager.openSpeaker();
|
||||
LogUtils.d(TAG, "setCallTypeUi: 去电模式,开启扬声器");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* 设置界面锁屏显示
|
||||
*/
|
||||
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: 设置界面支持锁屏显示");
|
||||
}
|
||||
|
||||
// ====================== 事件点击与计时方法区 ======================
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.tv_phone_pick_up) {
|
||||
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) {
|
||||
phoneCallManager.answer();
|
||||
tvPickUp.setVisibility(View.GONE);
|
||||
tvCallingTime.setVisibility(View.VISIBLE);
|
||||
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();
|
||||
stopTimer();
|
||||
startCallTimer();
|
||||
LogUtils.d(TAG, "answerCall: 接听通话成功");
|
||||
} else {
|
||||
LogUtils.e(TAG, "answerCall: 通话管理器为空,无法接听");
|
||||
}
|
||||
}
|
||||
|
||||
private String getCallingTime() {
|
||||
int minute = callingTime / 60;
|
||||
int second = callingTime % 60;
|
||||
return (minute < 10 ? "0" + minute : minute) +
|
||||
":" +
|
||||
(second < 10 ? "0" + second : second);
|
||||
/**
|
||||
* 挂断通话
|
||||
*/
|
||||
private void hangUpCall() {
|
||||
if (phoneCallManager != null) {
|
||||
phoneCallManager.disconnect();
|
||||
} else {
|
||||
LogUtils.e(TAG, "hangUpCall: 通话管理器为空,无法挂断");
|
||||
}
|
||||
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: 通话计时器停止");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
//MainActivity.updateCallLogFragment();
|
||||
phoneCallManager.destroy();
|
||||
/**
|
||||
* 格式化通话时长
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,58 +5,139 @@ import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.telecom.Call;
|
||||
import android.telecom.VideoProfile;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 通话管理工具类,负责接听、挂断通话及免提控制,仅支持Android 6.0(API 23)及以上系统
|
||||
*/
|
||||
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 call;
|
||||
|
||||
private Context context;
|
||||
private AudioManager audioManager;
|
||||
// ====================== 成员变量区 ======================
|
||||
// 静态通话实例,用于全局关联当前通话
|
||||
public static Call sCurrentCall;
|
||||
// 上下文对象
|
||||
private Context mContext;
|
||||
// 音频管理器,用于控制免提等音频相关操作
|
||||
private AudioManager mAudioManager;
|
||||
|
||||
// ====================== 构造方法区 ======================
|
||||
/**
|
||||
* 构造通话管理实例
|
||||
* @param context 上下文
|
||||
*/
|
||||
public PhoneCallManager(Context context) {
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
this.mContext = context;
|
||||
initAudioManager();
|
||||
LogUtils.d(TAG, "PhoneCallManager: 通话管理工具初始化");
|
||||
}
|
||||
|
||||
// ====================== 初始化辅助方法区 ======================
|
||||
/**
|
||||
* 接听电话
|
||||
* 初始化音频管理器
|
||||
*/
|
||||
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() {
|
||||
if (call != null) {
|
||||
call.answer(VideoProfile.STATE_AUDIO_ONLY);
|
||||
// 校验系统版本,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);
|
||||
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 (call != null) {
|
||||
call.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开免提
|
||||
* 打开通话免提
|
||||
*/
|
||||
public void openSpeaker() {
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_IN_CALL);
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 资源释放方法区 ======================
|
||||
/**
|
||||
* 销毁资源
|
||||
* 销毁资源,释放引用
|
||||
*/
|
||||
public void destroy() {
|
||||
call = null;
|
||||
context = null;
|
||||
audioManager = null;
|
||||
// 关闭免提后再释放资源,避免音频状态异常
|
||||
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: 通话管理工具资源已释放");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
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;
|
||||
@@ -17,199 +10,329 @@ 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.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
/**
|
||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 通话状态监听服务(需 Android 6.0+),负责通话状态回调、录音、铃音控制及黑白名单校验
|
||||
* @see PhoneCallActivity
|
||||
* @see android.telecom.InCallService
|
||||
*/
|
||||
public class PhoneCallService extends InCallService {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneCallService";
|
||||
// 强制指定正确常量值,彻底解决冲突
|
||||
// TelephonyManager 通话状态
|
||||
//private static final int CALL_STATE_OFFHOOK = TelephonyManager.CALL_STATE_OFFHOOK; // 固定值=2
|
||||
//private static final int CALL_STATE_IDLE = TelephonyManager.CALL_STATE_IDLE; // 固定值=0
|
||||
//private static final int CALL_STATE_RINGING_TELE = TelephonyManager.CALL_STATE_RINGING; // 固定值=1
|
||||
// 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
|
||||
|
||||
MediaRecorder mediaRecorder;
|
||||
// ====================== 成员变量区 ======================
|
||||
private MediaRecorder mMediaRecorder;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
// ====================== 生命周期方法区 ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
initCallCallback();
|
||||
LogUtils.d(TAG, "onCreate: 通话监听服务已创建");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAdded(Call call) {
|
||||
super.onCallAdded(call);
|
||||
|
||||
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.w(TAG, "onCallAdded: 新增通话为null,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
if (callType != null) {
|
||||
Call.Details details = call.getDetails();
|
||||
String phoneNumber = details.getHandle().getSchemeSpecificPart();
|
||||
call.registerCallback(mCallCallback);
|
||||
PhoneCallManager.sCurrentCall = call;
|
||||
LogUtils.d(TAG, "onCallAdded: 新增通话已注册回调 | 状态码=" + call.getState() + " | 描述=" + getCallStateDesc(call.getState()));
|
||||
|
||||
// 记录原始铃声音量
|
||||
//
|
||||
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);
|
||||
CallType callType = judgeCallType(call.getState());
|
||||
if (callType == null) {
|
||||
LogUtils.w(TAG, "onCallAdded: 无法识别通话类型 | 状态码=" + call.getState());
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
call.unregisterCallback(callback);
|
||||
PhoneCallManager.call = null;
|
||||
if (call != null) {
|
||||
call.unregisterCallback(mCallCallback);
|
||||
LogUtils.d(TAG, "onCallRemoved: 通话回调已注销");
|
||||
}
|
||||
PhoneCallManager.sCurrentCall = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
releaseRecorder();
|
||||
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));
|
||||
|
||||
switch (state) {
|
||||
// 仅合并值为2的两个状态,其他状态独立分支
|
||||
case CALL_STATE_ACTIVE:
|
||||
//case CALL_STATE_OFFHOOK:
|
||||
long callId = getCurrentCallId();
|
||||
if (callId != -1) {
|
||||
LogUtils.d(TAG, "onStateChanged: 通话接通/活跃,启动录音 | 通话ID=" + callId);
|
||||
startRecording(callId);
|
||||
} else {
|
||||
LogUtils.w(TAG, "onStateChanged: 未获取到通话ID,无法录音");
|
||||
}
|
||||
break;
|
||||
// 状态码1独立分支,彻底解决重复问题
|
||||
case CALL_STATE_RINGING:
|
||||
LogUtils.d(TAG, "onStateChanged: 通话处于响铃状态");
|
||||
break;
|
||||
case CALL_STATE_IDLE:
|
||||
LogUtils.d(TAG, "onStateChanged: 通话挂断,停止录音");
|
||||
stopRecording();
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 核心业务方法区 ======================
|
||||
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));
|
||||
private String getPhoneNumberFromCall(Call call) {
|
||||
try {
|
||||
mediaRecorder.prepare();
|
||||
mediaRecorder.start();
|
||||
} catch (IOException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
Call.Details details = call.getDetails();
|
||||
if (details == null || details.getHandle() == null) {
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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 void startRecording(long callId) {
|
||||
releaseRecorder();
|
||||
mMediaRecorder = new MediaRecorder();
|
||||
try {
|
||||
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
|
||||
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
||||
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
||||
String path = getOutputFilePath(callId);
|
||||
mMediaRecorder.setOutputFile(path);
|
||||
mMediaRecorder.prepare();
|
||||
mMediaRecorder.start();
|
||||
LogUtils.d(TAG, "startRecording: 录音启动成功 | 路径=" + path);
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
LogUtils.e(TAG, "startRecording: 录音启动失败", e);
|
||||
releaseRecorder();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopRecording() {
|
||||
if (mMediaRecorder != null) {
|
||||
try {
|
||||
mMediaRecorder.stop();
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.e(TAG, "stopRecording: 录音停止失败", e);
|
||||
} finally {
|
||||
releaseRecorder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getOutputFilePath(long callId) {
|
||||
LogUtils.d(TAG, "getOutputFilePath(...)");
|
||||
// 设置录音文件的保存路径
|
||||
File file = new File(getExternalFilesDir(TAG), String.format("call_%d.mp4", callId));
|
||||
return file.getAbsolutePath();
|
||||
File dir = getExternalFilesDir(TAG);
|
||||
if (dir == null) {
|
||||
dir = new File(getFilesDir(), TAG);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
}
|
||||
return new File(dir, String.format("call_%d.mp4", callId)).getAbsolutePath();
|
||||
}
|
||||
|
||||
private void stopRecording() {
|
||||
LogUtils.d(TAG, "stopRecording()");
|
||||
if (mediaRecorder != null) {
|
||||
mediaRecorder.stop();
|
||||
mediaRecorder.release();
|
||||
mediaRecorder = null;
|
||||
private void releaseRecorder() {
|
||||
if (mMediaRecorder != null) {
|
||||
mMediaRecorder.release();
|
||||
mMediaRecorder = null;
|
||||
LogUtils.d(TAG, "releaseRecorder: 录音资源已释放");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 辅助工具方法区 ======================
|
||||
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";
|
||||
ContentResolver resolver = getApplicationContext().getContentResolver();
|
||||
if (resolver == null) {
|
||||
LogUtils.w(TAG, "getCurrentCallId: 内容解析器为null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
Uri uri = CallLog.Calls.CONTENT_URI;
|
||||
String[] projection = {CallLog.Calls._ID};
|
||||
String selection = CallLog.Calls.TYPE + " = " + CallLog.Calls.OUTGOING_TYPE
|
||||
+ " OR " + CallLog.Calls.TYPE + " = " + CallLog.Calls.INCOMING_TYPE;
|
||||
String sort = CallLog.Calls.DATE + " DESC";
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
Cursor cursor = contentResolver.query(callLogUri, projection, selection, null, sortOrder);
|
||||
cursor = resolver.query(uri, projection, selection, null, sort);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndex("_id"));
|
||||
long id = cursor.getLong(cursor.getColumnIndex(CallLog.Calls._ID));
|
||||
LogUtils.d(TAG, "getCurrentCallId: 通话ID=" + id);
|
||||
return id;
|
||||
}
|
||||
LogUtils.w(TAG, "getCurrentCallId: 未查询到通话记录");
|
||||
return -1;
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getCurrentCallId: 查询失败(权限不足)", e);
|
||||
return -1;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
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_OFFHOOK:
|
||||
// return "摘机";
|
||||
case CALL_STATE_IDLE:
|
||||
return "空闲";
|
||||
case CALL_STATE_DISCONNECTED:
|
||||
return "已断开";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,61 +12,87 @@ import java.lang.ref.WeakReference;
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:58:04
|
||||
* @Describe 主要广播接收器,监听系统开机广播并启动主服务
|
||||
* @Describe 主要广播接收器,监听系统开机广播并自动启动主服务
|
||||
*/
|
||||
public class MainReceiver extends BroadcastReceiver {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainReceiver";
|
||||
public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
// 监听的系统广播 Action
|
||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private WeakReference<MainService> mwrService;
|
||||
// 使用弱引用关联 MainService,避免内存泄漏
|
||||
private WeakReference<MainService> mMainServiceWeakRef;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public MainReceiver(MainService service) {
|
||||
this.mwrService = new WeakReference<MainService>(service);
|
||||
LogUtils.d(TAG, "MainReceiver: 初始化广播接收器,关联MainService");
|
||||
this.mMainServiceWeakRef = new WeakReference<>(service);
|
||||
LogUtils.d(TAG, "MainReceiver: 初始化完成,已关联 MainService 实例");
|
||||
}
|
||||
|
||||
// ====================== 重写 BroadcastReceiver 方法区 ======================
|
||||
// ====================== 重写 BroadcastReceiver 核心方法 ======================
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// 空值校验,避免空指针异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "onReceive: Context 为 null,无法处理广播");
|
||||
return;
|
||||
}
|
||||
if (intent == null || intent.getAction() == null) {
|
||||
LogUtils.w(TAG, "onReceive: 接收到空Intent或空Action");
|
||||
LogUtils.w(TAG, "onReceive: 接收到空 Intent 或空 Action");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
LogUtils.d(TAG, "onReceive: 接收到广播,Action=" + action);
|
||||
LogUtils.d(TAG, "onReceive: 接收到广播 | Action=" + action);
|
||||
|
||||
// 处理开机完成广播
|
||||
if (ACTION_BOOT_COMPLETED.equals(action)) {
|
||||
LogUtils.d(TAG, "onReceive: 监听到开机广播,启动MainService");
|
||||
ToastUtils.show("ACTION_BOOT_COMPLETED");
|
||||
LogUtils.i(TAG, "onReceive: 监听到开机完成广播,自动启动 MainService");
|
||||
ToastUtils.show("设备开机,启动拨号主服务");
|
||||
MainService.startMainService(context);
|
||||
} else {
|
||||
LogUtils.i(TAG, "onReceive: 接收到未知广播,Action=" + action);
|
||||
ToastUtils.show(action);
|
||||
LogUtils.i(TAG, "onReceive: 接收到未处理的广播 | Action=" + action);
|
||||
ToastUtils.show("收到广播:" + action);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 公共方法区 ======================
|
||||
// ====================== 广播注册/注销方法区 ======================
|
||||
/**
|
||||
* 注册广播接收器,监听指定系统广播
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
public void registerAction(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "registerAction: 上下文对象为null,注册失败");
|
||||
LogUtils.e(TAG, "registerAction: Context 为 null,注册失败");
|
||||
return;
|
||||
}
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_BOOT_COMPLETED);
|
||||
// 原注释的铃声模式变更广播可按需启用
|
||||
// filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(ACTION_BOOT_COMPLETED);
|
||||
// 可按需添加其他监听的 Action
|
||||
// intentFilter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
||||
|
||||
context.registerReceiver(this, filter);
|
||||
LogUtils.d(TAG, "registerAction: 广播接收器注册成功,监听Action=" + ACTION_BOOT_COMPLETED);
|
||||
context.registerReceiver(this, intentFilter);
|
||||
LogUtils.d(TAG, "registerAction: 广播接收器注册成功 | 监听 Action=" + ACTION_BOOT_COMPLETED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销广播接收器,释放资源(解决 mMainReceiver.unregisterAction(this) 调用缺失问题)
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
public void unregisterAction(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "unregisterAction: Context 为 null,注销失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(this);
|
||||
LogUtils.d(TAG, "unregisterAction: 广播接收器注销成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "unregisterAction: 广播接收器未注册,无需注销", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -13,125 +8,191 @@ import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:38:31
|
||||
* @Describe 守护进程服务,用于监控并保活主服务 MainService
|
||||
*/
|
||||
public class AssistantService extends Service {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AssistantService";
|
||||
|
||||
MainServiceBean mMainServiceBean;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
MainService mMainService;
|
||||
boolean isBound = false;
|
||||
volatile boolean isThreadAlive = false;
|
||||
// ====================== 成员变量区 ======================
|
||||
private MainServiceBean mMainServiceBean;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private MainService mMainService;
|
||||
private boolean mIsBound = false;
|
||||
private volatile boolean mIsThreadAlive = false;
|
||||
|
||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
||||
LogUtils.d(TAG, "setIsThreadAlive(...)");
|
||||
LogUtils.d(TAG, String.format("isThreadAlive %s", isThreadAlive));
|
||||
this.isThreadAlive = isThreadAlive;
|
||||
// ====================== Binder 内部类 ======================
|
||||
/**
|
||||
* 对外暴露服务实例的 Binder
|
||||
*/
|
||||
public class MyBinder extends Binder {
|
||||
public AssistantService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 获取 AssistantService 实例");
|
||||
return AssistantService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== ServiceConnection 内部类 ======================
|
||||
/**
|
||||
* 主服务连接状态监听回调
|
||||
*/
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (service == null) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的 IBinder 为 null");
|
||||
mIsBound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
||||
mMainService = binder.getService();
|
||||
mIsBound = true;
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 主服务绑定成功 | MainService=" + mMainService);
|
||||
} catch (ClassCastException e) {
|
||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e);
|
||||
mIsBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务连接断开");
|
||||
mMainService = null;
|
||||
mIsBound = false;
|
||||
|
||||
// 尝试重新绑定主服务(如果配置为启用)
|
||||
reloadMainServiceConfig();
|
||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 重新唤醒并绑定主服务");
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外方法区 ======================
|
||||
/**
|
||||
* 设置线程存活状态
|
||||
*/
|
||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
||||
this.mIsThreadAlive = isThreadAlive;
|
||||
LogUtils.d(TAG, "setIsThreadAlive: 线程存活状态变更 | " + isThreadAlive);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取线程存活状态
|
||||
*/
|
||||
public boolean isThreadAlive() {
|
||||
return isThreadAlive;
|
||||
return mIsThreadAlive;
|
||||
}
|
||||
|
||||
// ====================== Service 生命周期方法区 ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate: 守护服务创建");
|
||||
|
||||
// 初始化主服务连接回调
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
LogUtils.d(TAG, "onCreate: 初始化 MyServiceConnection 完成");
|
||||
}
|
||||
|
||||
// 初始化运行状态
|
||||
setIsThreadAlive(false);
|
||||
// 启动守护逻辑
|
||||
assistantService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
super.onCreate();
|
||||
|
||||
//mMyBinder = new MyBinder();
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
// 设置运行参数
|
||||
setIsThreadAlive(false);
|
||||
assistantService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "call onStartCommand(...)");
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
|
||||
// 每次启动都执行守护逻辑,确保主服务存活
|
||||
assistantService();
|
||||
// START_STICKY:服务被杀死后系统尝试重启
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
setIsThreadAlive(false);
|
||||
// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁");
|
||||
|
||||
// 停止线程并解除主服务绑定
|
||||
setIsThreadAlive(false);
|
||||
if (mIsBound && mMyServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mMyServiceConnection);
|
||||
LogUtils.d(TAG, "onDestroy: 解除主服务绑定成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e);
|
||||
}
|
||||
mIsBound = false;
|
||||
}
|
||||
mMainService = null;
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
LogUtils.d(TAG, String.format("mMainServiceBean.isEnable() %s", mMainServiceBean.isEnable()));
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, String.format("mIsThreadAlive %s", isThreadAlive()));
|
||||
if (isThreadAlive() == false) {
|
||||
// 设置运行状态
|
||||
setIsThreadAlive(true);
|
||||
// 唤醒和绑定主进程
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
// ====================== 核心守护逻辑方法区 ======================
|
||||
/**
|
||||
* 守护服务核心逻辑:检查配置并保活主服务
|
||||
*/
|
||||
private void assistantService() {
|
||||
LogUtils.d(TAG, "assistantService: 执行守护逻辑");
|
||||
|
||||
// 加载主服务配置
|
||||
reloadMainServiceConfig();
|
||||
if (mMainServiceBean == null) {
|
||||
LogUtils.e(TAG, "assistantService: 主服务配置加载失败,终止守护逻辑");
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "assistantService: 主服务启用状态 | " + mMainServiceBean.isEnable());
|
||||
// 配置启用且线程未存活时,唤醒并绑定主服务
|
||||
if (mMainServiceBean.isEnable() && !isThreadAlive()) {
|
||||
setIsThreadAlive(true);
|
||||
wakeupAndBindMain();
|
||||
} else if (!mMainServiceBean.isEnable()) {
|
||||
setIsThreadAlive(false);
|
||||
LogUtils.d(TAG, "assistantService: 主服务已禁用,停止保活");
|
||||
}
|
||||
}
|
||||
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
LogUtils.d(TAG, "wakeupAndBindMain()");
|
||||
// 绑定服务的Intent
|
||||
/**
|
||||
* 唤醒并绑定主服务 MainService
|
||||
*/
|
||||
private void wakeupAndBindMain() {
|
||||
if (mMyServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindMain: MyServiceConnection 未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, MainService.class);
|
||||
startService(new Intent(this, MainService.class));
|
||||
// 先启动主服务,再绑定(确保服务进程存在)
|
||||
startService(intent);
|
||||
// BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// startService(new Intent(this, MainService.class));
|
||||
// bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService");
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
LogUtils.d(TAG, "onServiceConnected(...)");
|
||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
||||
mMainService = binder.getService();
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected(...)");
|
||||
mMainServiceBean = MainServiceBean.loadBean(AssistantService.this, MainServiceBean.class);
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
isBound = false;
|
||||
mMainService = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于返回服务实例的Binder
|
||||
public class MyBinder extends Binder {
|
||||
AssistantService getService() {
|
||||
LogUtils.d(TAG, "AssistantService MyBinder getService()");
|
||||
return AssistantService.this;
|
||||
}
|
||||
// ====================== 辅助方法区 ======================
|
||||
/**
|
||||
* 重新加载主服务配置
|
||||
*/
|
||||
private void reloadMainServiceConfig() {
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
LogUtils.d(TAG, "reloadMainServiceConfig: 主服务配置重新加载完成 | " + mMainServiceBean);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
package cc.winboll.studio.contacts.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:56:41
|
||||
* @Describe 拨号主服务
|
||||
* 参考:
|
||||
* 进程保活-双进程守护的正确姿势
|
||||
* https://blog.csdn.net/sinat_35159441/article/details/75267380
|
||||
* Android Service之onStartCommand方法研究
|
||||
* https://blog.csdn.net/cyp331203/article/details/38920491
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -18,305 +8,420 @@ import android.content.ServiceConnection;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
|
||||
import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
|
||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||
import cc.winboll.studio.contacts.receivers.MainReceiver;
|
||||
import cc.winboll.studio.contacts.services.MainService;
|
||||
import cc.winboll.studio.contacts.threads.MainServiceThread;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/13 06:56:41
|
||||
* @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动
|
||||
* 参考:
|
||||
* 进程保活-双进程守护的正确姿势
|
||||
* Android Service之onStartCommand方法研究
|
||||
*/
|
||||
public class MainService extends Service {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainService";
|
||||
|
||||
public static final int MSG_UPDATE_STATUS = 0;
|
||||
// 铃声音量检查定时器参数:延迟1秒启动,每分钟检查一次
|
||||
private static final long VOLUME_CHECK_DELAY = 1000L;
|
||||
private static final long VOLUME_CHECK_PERIOD = 60000L;
|
||||
|
||||
static MainService _mControlCenterService;
|
||||
// ====================== 静态成员变量区 ======================
|
||||
private static MainService sMainServiceInstance;
|
||||
private static volatile TomCat sTomCatInstance;
|
||||
|
||||
volatile boolean isServiceRunning;
|
||||
// ====================== 成员变量区 ======================
|
||||
private volatile boolean mIsServiceRunning;
|
||||
private MainServiceBean mMainServiceBean;
|
||||
private MainServiceThread mMainServiceThread;
|
||||
private MainServiceHandler mMainServiceHandler;
|
||||
private MyServiceConnection mMyServiceConnection;
|
||||
private AssistantService mAssistantService;
|
||||
private boolean mIsBound;
|
||||
private MainReceiver mMainReceiver;
|
||||
private Timer mStreamVolumeCheckTimer;
|
||||
|
||||
MainServiceBean mMainServiceBean;
|
||||
MainServiceThread mMainServiceThread;
|
||||
MainServiceHandler mMainServiceHandler;
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
AssistantService mAssistantService;
|
||||
boolean isBound = false;
|
||||
MainReceiver mMainReceiver;
|
||||
Timer mStreamVolumeCheckTimer;
|
||||
static volatile TomCat _TomCat;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
public MainServiceThread getRemindThread() {
|
||||
return mMainServiceThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate()");
|
||||
_mControlCenterService = MainService.this;
|
||||
isServiceRunning = false;
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
mMainServiceHandler = new MainServiceHandler(this);
|
||||
|
||||
// 铃声检查定时器
|
||||
mStreamVolumeCheckTimer = new Timer();
|
||||
mStreamVolumeCheckTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
int ringerVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
// 恢复铃声音量,预防其他意外条件导致的音量变化问题
|
||||
//
|
||||
|
||||
// 读取应用配置,未配置就初始化配置文件
|
||||
RingTongBean bean = RingTongBean.loadBean(MainService.this, RingTongBean.class);
|
||||
if (bean == null) {
|
||||
// 初始化配置
|
||||
bean = new RingTongBean();
|
||||
RingTongBean.saveBean(MainService.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());
|
||||
}
|
||||
}
|
||||
}, 1000, 60000);
|
||||
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand(...)");
|
||||
// 运行服务内容
|
||||
mainService();
|
||||
return (mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
// 运行服务内容
|
||||
//
|
||||
void mainService() {
|
||||
LogUtils.d(TAG, "mainService()");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean.isEnable() && isServiceRunning == false) {
|
||||
LogUtils.d(TAG, "mainService() start running");
|
||||
isServiceRunning = true;
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
// 召唤 WinBoLL APP 绑定本服务
|
||||
// if (App.isDebugging()) {
|
||||
// WinBoLL.bindToAPPBaseBeta(this, MainService.class.getName());
|
||||
// } else {
|
||||
// WinBoLL.bindToAPPBase(this, MainService.class.getName());
|
||||
// }
|
||||
|
||||
// 初始化服务运行参数
|
||||
_TomCat = TomCat.getInstance(this);
|
||||
if (!_TomCat.loadPhoneBoBullToon()) {
|
||||
LogUtils.d(TAG, "没有下载 BoBullToon 数据。BoBullToon 参数无法加载。");
|
||||
}
|
||||
|
||||
if (mMainReceiver == null) {
|
||||
// 注册广播接收器
|
||||
mMainReceiver = new MainReceiver(this);
|
||||
mMainReceiver.registerAction(this);
|
||||
}
|
||||
|
||||
Rules.getInstance(this).loadRules();
|
||||
|
||||
startPhoneCallListener();
|
||||
|
||||
MainServiceThread.getInstance(this, mMainServiceHandler).start();
|
||||
|
||||
LogUtils.i(TAG, "Main Service Is Start.");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPhoneInBoBullToon(String phone) {
|
||||
if (_TomCat != null) {
|
||||
return _TomCat.isPhoneBoBullToon(phone);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 唤醒和绑定守护进程
|
||||
//
|
||||
void wakeupAndBindAssistant() {
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistant()");
|
||||
// if (ServiceUtils.isServiceAlive(getApplicationContext(), AssistantService.class.getName()) == false) {
|
||||
// startService(new Intent(MainService.this, AssistantService.class));
|
||||
// //LogUtils.d(TAG, "call wakeupAndBindAssistant() : Binding... AssistantService");
|
||||
// bindService(new Intent(MainService.this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
// }
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
startService(intent);
|
||||
// 绑定服务的Intent
|
||||
//Intent intent = new Intent(this, AssistantService.class);
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
|
||||
// Intent intent = new Intent(this, AssistantService.class);
|
||||
// startService(intent);
|
||||
// LogUtils.d(TAG, "startService(intent)");
|
||||
// bindService(new Intent(this, AssistantService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
|
||||
void startPhoneCallListener() {
|
||||
Intent callListener = new Intent(this, CallListenerService.class);
|
||||
startService(callListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
//LogUtils.d(TAG, "onDestroy");
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
//LogUtils.d(TAG, "onDestroy done");
|
||||
if (mMainServiceBean.isEnable() == false) {
|
||||
// 设置运行状态
|
||||
isServiceRunning = false;// 解除绑定
|
||||
if (isBound) {
|
||||
unbindService(mMyServiceConnection);
|
||||
isBound = false;
|
||||
}
|
||||
// 停止守护进程
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
stopService(intent);
|
||||
// 停止Receiver
|
||||
if (mMainReceiver != null) {
|
||||
unregisterReceiver(mMainReceiver);
|
||||
mMainReceiver = null;
|
||||
}
|
||||
// 停止前台通知栏
|
||||
stopForeground(true);
|
||||
|
||||
// 停止主要进程
|
||||
MainServiceThread.getInstance(this, mMainServiceHandler).setIsExit(true);
|
||||
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
LogUtils.d(TAG, "onServiceConnected(...)");
|
||||
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
|
||||
mAssistantService = binder.getService();
|
||||
isBound = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.d(TAG, "onServiceDisconnected(...)");
|
||||
if (mMainServiceBean.isEnable()) {
|
||||
// 唤醒守护进程
|
||||
wakeupAndBindAssistant();
|
||||
// if (App.isDebuging()) {
|
||||
// SOS.sosToAppBase(getApplicationContext(), MainService.class.getName());
|
||||
// } else {
|
||||
// SOS.sosToAppBaseBeta(getApplicationContext(), MainService.class.getName());
|
||||
// }
|
||||
}
|
||||
isBound = false;
|
||||
mAssistantService = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 用于返回服务实例的Binder
|
||||
// ====================== Binder 内部类 ======================
|
||||
/**
|
||||
* 对外暴露服务实例的 Binder
|
||||
*/
|
||||
public class MyBinder extends Binder {
|
||||
MainService getService() {
|
||||
LogUtils.d(TAG, "MainService MyBinder getService()");
|
||||
public MainService getService() {
|
||||
LogUtils.d(TAG, "MyBinder.getService: 获取 MainService 实例");
|
||||
return MainService.this;
|
||||
}
|
||||
}
|
||||
|
||||
// //
|
||||
// // 启动服务
|
||||
// //
|
||||
// public static void startControlCenterService(Context context) {
|
||||
// Intent intent = new Intent(context, MainService.class);
|
||||
// context.startForegroundService(intent);
|
||||
// }
|
||||
//
|
||||
// //
|
||||
// // 停止服务
|
||||
// //
|
||||
// public static void stopControlCenterService(Context context) {
|
||||
// Intent intent = new Intent(context, MainService.class);
|
||||
// context.stopService(intent);
|
||||
// }
|
||||
// ====================== ServiceConnection 内部类 ======================
|
||||
/**
|
||||
* 守护服务连接状态监听回调
|
||||
*/
|
||||
private class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
if (service == null) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的 IBinder 为 null");
|
||||
mIsBound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
public void appenMessage(String message) {
|
||||
LogUtils.d(TAG, String.format("Message : %s", message));
|
||||
}
|
||||
try {
|
||||
AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
|
||||
mAssistantService = binder.getService();
|
||||
mIsBound = true;
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 守护服务绑定成功 | AssistantService=" + mAssistantService);
|
||||
} catch (ClassCastException e) {
|
||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e);
|
||||
mIsBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMainService(Context context) {
|
||||
LogUtils.d(TAG, "stopMainService");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 守护服务连接断开");
|
||||
mAssistantService = null;
|
||||
mIsBound = false;
|
||||
|
||||
public static void startMainService(Context context) {
|
||||
LogUtils.d(TAG, "startMainService");
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
public static void restartMainService(Context context) {
|
||||
LogUtils.d(TAG, "restartMainService");
|
||||
|
||||
MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class);
|
||||
if (bean != null && bean.isEnable()) {
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
// try {
|
||||
// Thread.sleep(1000);
|
||||
// } catch (InterruptedException e) {
|
||||
// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
// }
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
LogUtils.d(TAG, "已重启 MainService");
|
||||
// 尝试重新绑定守护服务(如果主服务配置为启用)
|
||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 重新唤醒并绑定守护服务");
|
||||
wakeupAndBindAssistant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
// ====================== 对外静态方法区 ======================
|
||||
/**
|
||||
* 判断号码是否在 BoBullToon 数据中
|
||||
*/
|
||||
public static boolean isPhoneInBoBullToon(String phone) {
|
||||
if (sTomCatInstance != null && phone != null && !phone.isEmpty()) {
|
||||
return sTomCatInstance.isPhoneBoBullToon(phone);
|
||||
}
|
||||
LogUtils.w(TAG, "isPhoneInBoBullToon: TomCat 未初始化或号码为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止主服务
|
||||
*/
|
||||
public static void stopMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopMainService: Context 为 null,无法执行");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "stopMainService: 执行停止主服务操作");
|
||||
context.stopService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动主服务
|
||||
*/
|
||||
public static void startMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startMainService: Context 为 null,无法执行");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "startMainService: 执行启动主服务操作");
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启主服务(仅配置启用时执行)
|
||||
*/
|
||||
public static void restartMainService(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "restartMainService: Context 为 null,无法执行");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "restartMainService: 执行重启主服务操作");
|
||||
|
||||
MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class);
|
||||
if (bean != null && bean.isEnable()) {
|
||||
stopMainService(context);
|
||||
startMainService(context);
|
||||
LogUtils.i(TAG, "restartMainService: 主服务重启完成");
|
||||
} else {
|
||||
LogUtils.w(TAG, "restartMainService: 主服务配置未启用,跳过重启");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止主服务并保存禁用状态
|
||||
*/
|
||||
public static void stopMainServiceAndSaveStatus(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "stopMainServiceAndSaveStatus: Context 为 null,无法执行");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "stopMainServiceAndSaveStatus: 停止服务并保存禁用状态");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(false);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
stopMainService(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动主服务并保存启用状态
|
||||
*/
|
||||
public static void startMainServiceAndSaveStatus(Context context) {
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus");
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "startMainServiceAndSaveStatus: Context 为 null,无法执行");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "startMainServiceAndSaveStatus: 启动服务并保存启用状态");
|
||||
MainServiceBean bean = new MainServiceBean();
|
||||
bean.setIsEnable(true);
|
||||
MainServiceBean.saveBean(context, bean);
|
||||
context.startService(new Intent(context, MainService.class));
|
||||
startMainService(context);
|
||||
}
|
||||
|
||||
// ====================== 成员方法区 ======================
|
||||
/**
|
||||
* 获取提醒线程实例
|
||||
*/
|
||||
public MainServiceThread getRemindThread() {
|
||||
return mMainServiceThread;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加日志消息
|
||||
*/
|
||||
public void appenMessage(String message) {
|
||||
LogUtils.d(TAG, "Message : " + (message == null ? "null" : message));
|
||||
}
|
||||
|
||||
// ====================== Service 生命周期方法区 ======================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate: 主服务创建");
|
||||
sMainServiceInstance = this;
|
||||
mIsServiceRunning = false;
|
||||
|
||||
// 初始化配置与核心组件
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
mMainServiceHandler = new MainServiceHandler(this);
|
||||
|
||||
// 初始化铃声音量检查定时器
|
||||
initVolumeCheckTimer();
|
||||
|
||||
// 启动主服务核心逻辑
|
||||
mainService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
|
||||
return new MyBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
|
||||
// 每次启动都执行核心逻辑,确保服务状态正确
|
||||
mainService();
|
||||
// 配置启用时返回 START_STICKY 保活,否则使用默认返回值
|
||||
return (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy: 主服务销毁");
|
||||
|
||||
// 释放定时器资源
|
||||
cancelVolumeCheckTimer();
|
||||
|
||||
// 仅配置禁用时执行资源释放逻辑
|
||||
if (mMainServiceBean != null && !mMainServiceBean.isEnable()) {
|
||||
mIsServiceRunning = false;
|
||||
// 解除守护服务绑定
|
||||
if (mIsBound && mMyServiceConnection != null) {
|
||||
try {
|
||||
unbindService(mMyServiceConnection);
|
||||
LogUtils.d(TAG, "onDestroy: 解除守护服务绑定成功");
|
||||
} catch (IllegalArgumentException e) {
|
||||
LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e);
|
||||
}
|
||||
mIsBound = false;
|
||||
}
|
||||
|
||||
// 注销广播接收器
|
||||
if (mMainReceiver != null) {
|
||||
mMainReceiver.unregisterAction(this);
|
||||
mMainReceiver = null;
|
||||
LogUtils.d(TAG, "onDestroy: 广播接收器已注销");
|
||||
}
|
||||
|
||||
// 停止主线程
|
||||
if (mMainServiceThread != null) {
|
||||
mMainServiceThread.setIsExit(true);
|
||||
mMainServiceThread = null;
|
||||
}
|
||||
|
||||
// 停止守护服务
|
||||
stopService(new Intent(this, AssistantService.class));
|
||||
}
|
||||
|
||||
// 清空静态实例
|
||||
sMainServiceInstance = null;
|
||||
sTomCatInstance = null;
|
||||
}
|
||||
|
||||
// ====================== 核心业务逻辑方法区 ======================
|
||||
/**
|
||||
* 主服务核心逻辑:初始化组件、绑定守护服务、启动业务线程
|
||||
*/
|
||||
private void mainService() {
|
||||
LogUtils.d(TAG, "mainService: 执行核心业务逻辑");
|
||||
// 重新加载配置,确保使用最新状态
|
||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||
if (mMainServiceBean == null || !mMainServiceBean.isEnable() || mIsServiceRunning) {
|
||||
LogUtils.d(TAG, "mainService: 无需启动 | 配置启用=" + (mMainServiceBean != null && mMainServiceBean.isEnable()) + " | 服务运行中=" + mIsServiceRunning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记服务为运行状态
|
||||
mIsServiceRunning = true;
|
||||
LogUtils.i(TAG, "mainService: 主服务开始运行");
|
||||
|
||||
// 绑定守护服务保活
|
||||
wakeupAndBindAssistant();
|
||||
|
||||
// 初始化 TomCat 与号码数据
|
||||
initTomCat();
|
||||
|
||||
// 注册广播接收器
|
||||
initMainReceiver();
|
||||
|
||||
// 加载黑白名单规则
|
||||
Rules.getInstance(this).loadRules();
|
||||
LogUtils.d(TAG, "mainService: 黑白名单规则已加载");
|
||||
|
||||
// 启动通话监听服务
|
||||
startPhoneCallListener();
|
||||
|
||||
// 启动主业务线程
|
||||
mMainServiceThread = MainServiceThread.getInstance(this, mMainServiceHandler);
|
||||
mMainServiceThread.start();
|
||||
LogUtils.i(TAG, "mainService: 主业务线程已启动");
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤醒并绑定守护服务
|
||||
*/
|
||||
private void wakeupAndBindAssistant() {
|
||||
if (mMyServiceConnection == null) {
|
||||
LogUtils.e(TAG, "wakeupAndBindAssistant: MyServiceConnection 未初始化,绑定失败");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, AssistantService.class);
|
||||
startService(intent);
|
||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
LogUtils.d(TAG, "wakeupAndBindAssistant: 已启动并绑定守护服务");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动通话监听服务
|
||||
*/
|
||||
private void startPhoneCallListener() {
|
||||
Intent callListenerIntent = new Intent(this, CallListenerService.class);
|
||||
startService(callListenerIntent);
|
||||
LogUtils.d(TAG, "startPhoneCallListener: 通话监听服务已启动");
|
||||
}
|
||||
|
||||
// ====================== 铃声音量监控相关方法区 ======================
|
||||
/**
|
||||
* 初始化铃声音量检查定时器
|
||||
*/
|
||||
private void initVolumeCheckTimer() {
|
||||
cancelVolumeCheckTimer();
|
||||
mStreamVolumeCheckTimer = new Timer();
|
||||
mStreamVolumeCheckTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
checkAndRestoreRingerVolume();
|
||||
}
|
||||
}, VOLUME_CHECK_DELAY, VOLUME_CHECK_PERIOD);
|
||||
LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量检查定时器已启动");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并恢复铃声音量至配置值
|
||||
*/
|
||||
private void checkAndRestoreRingerVolume() {
|
||||
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
if (audioManager == null) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: 获取 AudioManager 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载铃音配置(无配置则初始化)
|
||||
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
||||
if (ringTongBean == null) {
|
||||
ringTongBean = new RingTongBean();
|
||||
RingTongBean.saveBean(this, ringTongBean);
|
||||
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃音配置未存在,已初始化默认配置");
|
||||
}
|
||||
|
||||
// 检查并恢复音量
|
||||
try {
|
||||
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||
int configVolume = ringTongBean.getStreamVolume();
|
||||
if (currentVolume != configVolume) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
||||
LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃声音量已恢复 | 配置值=" + configVolume + " | 当前值=" + currentVolume);
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消铃声音量检查定时器
|
||||
*/
|
||||
private void cancelVolumeCheckTimer() {
|
||||
if (mStreamVolumeCheckTimer != null) {
|
||||
mStreamVolumeCheckTimer.cancel();
|
||||
mStreamVolumeCheckTimer = null;
|
||||
LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量检查定时器已取消");
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 辅助初始化方法区 ======================
|
||||
/**
|
||||
* 初始化 TomCat 与 BoBullToon 数据
|
||||
*/
|
||||
private void initTomCat() {
|
||||
sTomCatInstance = TomCat.getInstance(this);
|
||||
if (!sTomCatInstance.loadPhoneBoBullToon()) {
|
||||
LogUtils.w(TAG, "initTomCat: 未下载 BoBullToon 数据,参数加载失败");
|
||||
} else {
|
||||
LogUtils.d(TAG, "initTomCat: BoBullToon 数据加载成功");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化广播接收器
|
||||
*/
|
||||
private void initMainReceiver() {
|
||||
if (mMainReceiver == null) {
|
||||
mMainReceiver = new MainReceiver(this);
|
||||
mMainReceiver.registerAction(this);
|
||||
LogUtils.d(TAG, "initMainReceiver: 广播接收器已注册");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,73 +1,104 @@
|
||||
package cc.winboll.studio.contacts.threads;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:46:44
|
||||
*/
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.contacts.handlers.MainServiceHandler;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/14 03:46:44
|
||||
* @Describe 主服务后台工作线程,负责定时轮询与消息调度
|
||||
*/
|
||||
public class MainServiceThread extends Thread {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "MainServiceThread";
|
||||
|
||||
volatile static MainServiceThread _MainServiceThread;
|
||||
// 控制线程是否退出的标志
|
||||
volatile boolean isExit = false;
|
||||
volatile boolean isStarted = false;
|
||||
Context mContext;
|
||||
// 服务Handler, 用于线程发送消息使用
|
||||
WeakReference<MainServiceHandler> mwrMainServiceHandler;
|
||||
// 线程休眠周期(1秒)
|
||||
private static final long THREAD_SLEEP_INTERVAL = 1000L;
|
||||
|
||||
MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
mContext = context;
|
||||
mwrMainServiceHandler = new WeakReference<MainServiceHandler>(handler);
|
||||
// ====================== 静态成员变量区 ======================
|
||||
private static volatile MainServiceThread sInstance;
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 线程运行控制标记
|
||||
private volatile boolean mIsExit;
|
||||
private volatile boolean mIsStarted;
|
||||
// 弱引用持有上下文和Handler,避免内存泄漏
|
||||
private WeakReference<Context> mContextWeakRef;
|
||||
private WeakReference<MainServiceHandler> mHandlerWeakRef;
|
||||
|
||||
// ====================== 私有构造函数 ======================
|
||||
private MainServiceThread(Context context, MainServiceHandler handler) {
|
||||
this.mContextWeakRef = new WeakReference<>(context);
|
||||
this.mHandlerWeakRef = new WeakReference<>(handler);
|
||||
this.mIsExit = false;
|
||||
this.mIsStarted = false;
|
||||
LogUtils.d(TAG, "MainServiceThread: 线程实例初始化完成");
|
||||
}
|
||||
|
||||
// ====================== 单例获取方法 ======================
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
// 若已有实例,先标记退出并销毁旧实例
|
||||
if (sInstance != null) {
|
||||
LogUtils.d(TAG, "getInstance: 存在旧线程实例,标记退出");
|
||||
sInstance.setIsExit(true);
|
||||
sInstance = null;
|
||||
}
|
||||
// 创建新线程实例
|
||||
sInstance = new MainServiceThread(context, handler);
|
||||
LogUtils.d(TAG, "getInstance: 新线程实例已创建");
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// ====================== 运行状态控制方法 ======================
|
||||
public void setIsExit(boolean isExit) {
|
||||
this.isExit = isExit;
|
||||
this.mIsExit = isExit;
|
||||
LogUtils.d(TAG, "setIsExit: 线程退出标记已更新 | " + isExit);
|
||||
}
|
||||
|
||||
public boolean isExit() {
|
||||
return isExit;
|
||||
return mIsExit;
|
||||
}
|
||||
|
||||
public void setIsStarted(boolean isStarted) {
|
||||
this.isStarted = isStarted;
|
||||
this.mIsStarted = isStarted;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return isStarted;
|
||||
}
|
||||
|
||||
public static MainServiceThread getInstance(Context context, MainServiceHandler handler) {
|
||||
if (_MainServiceThread != null) {
|
||||
_MainServiceThread.setIsExit(true);
|
||||
}
|
||||
_MainServiceThread = new MainServiceThread(context, handler);
|
||||
return _MainServiceThread;
|
||||
return mIsStarted;
|
||||
}
|
||||
|
||||
// ====================== 线程核心执行方法 ======================
|
||||
@Override
|
||||
public void run() {
|
||||
if (isStarted == false) {
|
||||
isStarted = true;
|
||||
LogUtils.d(TAG, "run()");
|
||||
|
||||
while (!isExit()) {
|
||||
//ToastUtils.show("run");
|
||||
//LogUtils.d(TAG, "run()");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
}
|
||||
}
|
||||
_MainServiceThread = null;
|
||||
LogUtils.d(TAG, "run() exit");
|
||||
// 防止重复启动
|
||||
if (mIsStarted) {
|
||||
LogUtils.w(TAG, "run: 线程已启动,避免重复执行");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 标记线程启动状态
|
||||
mIsStarted = true;
|
||||
LogUtils.i(TAG, "run: 线程开始运行");
|
||||
|
||||
// 线程主循环
|
||||
while (!mIsExit) {
|
||||
try {
|
||||
// 此处可添加业务逻辑(如定时任务、消息分发)
|
||||
Thread.sleep(THREAD_SLEEP_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "run: 线程休眠被中断", e);
|
||||
// 恢复线程中断状态
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// 线程退出清理
|
||||
mIsStarted = false;
|
||||
mContextWeakRef.clear();
|
||||
mHandlerWeakRef.clear();
|
||||
sInstance = null;
|
||||
LogUtils.i(TAG, "run: 线程正常退出");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,270 +1,268 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/27 14:27
|
||||
* @Describe 调用应用属性设置页工具类
|
||||
* 来源:https://blog.csdn.net/zhuhai__yizhi/article/details/78737593
|
||||
* Created by zyy on 2018/3/12.
|
||||
* 直接跳转到权限后返回,可以监控权限授权情况,但是,跳转到应用详情页,无法监测权限情况
|
||||
* 是否要加以区分,若是应用详情页,则跳转回来后,onRestart检测所求权限,如果授权,则收回提示,如果没授权,则继续提示
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import cc.winboll.studio.contacts.MainActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/27 14:27
|
||||
* @Describe 应用权限设置页跳转工具类,适配主流手机厂商的权限页路径,跳转失败时降级到应用详情页
|
||||
*/
|
||||
public class AppGoToSettingsUtil {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "AppGoToSettingsUtil";
|
||||
|
||||
// 跳转设置页的 Activity 结果码,复用 MainActivity 的请求码
|
||||
public static final int ACTIVITY_RESULT_APP_SETTINGS = MainActivity.REQUEST_APP_SETTINGS;
|
||||
|
||||
/**
|
||||
* Build.MANUFACTURER判断各大手机厂商品牌
|
||||
*/
|
||||
private static final String MANUFACTURER_HUAWEI = "Huawei";//华为
|
||||
private static final String MANUFACTURER_MEIZU = "Meizu";//魅族
|
||||
private static final String MANUFACTURER_XIAOMI = "Xiaomi";//小米
|
||||
private static final String MANUFACTURER_SONY = "Sony";//索尼
|
||||
// 主流手机厂商品牌常量
|
||||
private static final String MANUFACTURER_HUAWEI = "Huawei";
|
||||
private static final String MANUFACTURER_MEIZU = "Meizu";
|
||||
private static final String MANUFACTURER_XIAOMI = "Xiaomi";
|
||||
private static final String MANUFACTURER_SONY = "Sony";
|
||||
private static final String MANUFACTURER_OPPO = "OPPO";
|
||||
private static final String MANUFACTURER_LG = "LG";
|
||||
private static final String MANUFACTURER_VIVO = "vivo";
|
||||
private static final String MANUFACTURER_SAMSUNG = "samsung";//三星
|
||||
private static final String MANUFACTURER_LETV = "Letv";//乐视
|
||||
private static final String MANUFACTURER_ZTE = "ZTE";//中兴
|
||||
private static final String MANUFACTURER_YULONG = "YuLong";//酷派
|
||||
private static final String MANUFACTURER_LENOVO = "LENOVO";//联想
|
||||
private static final String MANUFACTURER_SAMSUNG = "samsung";
|
||||
private static final String MANUFACTURER_LETV = "Letv";
|
||||
private static final String MANUFACTURER_ZTE = "ZTE";
|
||||
private static final String MANUFACTURER_YULONG = "YuLong";
|
||||
private static final String MANUFACTURER_LENOVO = "LENOVO";
|
||||
|
||||
public static boolean isAppSettingOpen=false;
|
||||
// ====================== 成员变量区 ======================
|
||||
// 标记当前跳转的是应用详情页(true)还是厂商权限页(false)
|
||||
public static boolean isAppSettingOpen = false;
|
||||
|
||||
// ====================== 核心跳转方法区 ======================
|
||||
/**
|
||||
* 跳转到相应品牌手机系统权限设置页,如果跳转不成功,则跳转到应用详情页
|
||||
* 这里需要改造成返回true或者false,应用详情页:true,应用权限页:false
|
||||
* @param activity
|
||||
* 跳转到对应品牌手机的系统权限设置页,跳转失败则降级到应用详情页
|
||||
* @param activity 上下文 Activity
|
||||
*/
|
||||
public static void GoToSetting(Activity activity) {
|
||||
switch (Build.MANUFACTURER) {
|
||||
case MANUFACTURER_HUAWEI://华为
|
||||
Huawei(activity);
|
||||
public static void goToSetting(Activity activity) {
|
||||
// 空值校验,避免空指针异常
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "goToSetting: Activity 为 null,无法跳转设置页");
|
||||
return;
|
||||
}
|
||||
|
||||
String manufacturer = Build.MANUFACTURER;
|
||||
LogUtils.d(TAG, "goToSetting: 当前设备厂商 | " + manufacturer);
|
||||
|
||||
// 根据厂商跳转对应权限页
|
||||
switch (manufacturer) {
|
||||
case MANUFACTURER_HUAWEI:
|
||||
gotoHuaweiSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_MEIZU://魅族
|
||||
Meizu(activity);
|
||||
case MANUFACTURER_MEIZU:
|
||||
gotoMeizuSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_XIAOMI://小米
|
||||
Xiaomi(activity);
|
||||
case MANUFACTURER_XIAOMI:
|
||||
gotoXiaomiSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_SONY://索尼
|
||||
Sony(activity);
|
||||
case MANUFACTURER_SONY:
|
||||
gotoSonySetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_OPPO://oppo
|
||||
OPPO(activity);
|
||||
case MANUFACTURER_OPPO:
|
||||
gotoOppoSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_LG://lg
|
||||
LG(activity);
|
||||
case MANUFACTURER_LG:
|
||||
gotoLgSetting(activity);
|
||||
break;
|
||||
case MANUFACTURER_LETV://乐视
|
||||
Letv(activity);
|
||||
case MANUFACTURER_LETV:
|
||||
gotoLetvSetting(activity);
|
||||
break;
|
||||
default://其他
|
||||
try {//防止应用详情页也找不到,捕获异常后跳转到设置,这里跳转最好是两级,太多用户也会觉得麻烦,还不如不跳
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
} catch (Exception e) {
|
||||
SystemConfig(activity);
|
||||
}
|
||||
default:
|
||||
LogUtils.w(TAG, "goToSetting: 未适配当前厂商,跳转应用详情页");
|
||||
openAppDetailSetting(activity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 各厂商权限页跳转方法区 ======================
|
||||
/**
|
||||
* 华为跳转权限设置页
|
||||
* @param activity
|
||||
* 跳转华为手机权限设置页
|
||||
*/
|
||||
public static void Huawei(Activity activity) {
|
||||
private static void gotoHuaweiSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.huawei.systemmanager",
|
||||
"com.huawei.permissionmanager.ui.MainActivity"));
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoHuaweiSetting: 跳转华为权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoHuaweiSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 魅族跳转权限设置页,测试时,点击无反应,具体原因不明
|
||||
* @param activity
|
||||
* 跳转魅族手机权限设置页
|
||||
*/
|
||||
public static void Meizu(Activity activity) {
|
||||
private static void gotoMeizuSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoMeizuSetting: 跳转魅族权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoMeizuSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小米,功能正常
|
||||
* @param activity
|
||||
* 跳转小米手机权限设置页
|
||||
*/
|
||||
public static void Xiaomi(Activity activity) {
|
||||
try { //MIUI 8 9
|
||||
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
|
||||
localIntent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
private static void gotoXiaomiSetting(Activity activity) {
|
||||
try {
|
||||
// 适配 MIUI 8/9 及以上版本
|
||||
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
intent.setClassName("com.miui.securitycenter",
|
||||
"com.miui.permcenter.permissions.PermissionsEditorActivity");
|
||||
intent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
//activity.startActivity(localIntent);
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI8+)成功");
|
||||
} catch (Exception e) {
|
||||
try { //MIUI 5/6/7
|
||||
Intent localIntent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
localIntent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
|
||||
localIntent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(localIntent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
try {
|
||||
// 适配 MIUI 5/6/7 版本
|
||||
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
|
||||
intent.setClassName("com.miui.securitycenter",
|
||||
"com.miui.permcenter.permissions.AppPermissionsEditorActivity");
|
||||
intent.putExtra("extra_pkgname", activity.getPackageName());
|
||||
activity.startActivityForResult(intent, ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = false;
|
||||
//activity.startActivity(localIntent);
|
||||
} catch (Exception e1) { //否则跳转到应用详情
|
||||
LogUtils.d(TAG, "gotoXiaomiSetting: 跳转小米权限设置页(MIUI5-7)成功");
|
||||
} catch (Exception e1) {
|
||||
LogUtils.e(TAG, "gotoXiaomiSetting: 所有版本适配失败,降级到应用详情页", e1);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
//这里有个问题,进入活动后需要再跳一级活动,就检测不到返回结果
|
||||
//activity.startActivity(getAppDetailSettingIntent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索尼,6.0以上的手机非常少,基本没看见
|
||||
* @param activity
|
||||
* 跳转索尼手机权限设置页
|
||||
*/
|
||||
public static void Sony(Activity activity) {
|
||||
private static void gotoSonySetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.sonymobile.cta", "com.sonymobile.cta.SomcCTAMainActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.sonymobile.cta",
|
||||
"com.sonymobile.cta.SomcCTAMainActivity"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoSonySetting: 跳转索尼权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoSonySetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OPPO
|
||||
* @param activity
|
||||
* 跳转OPPO手机权限设置页
|
||||
*/
|
||||
public static void OPPO(Activity activity) {
|
||||
private static void gotoOppoSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.color.safecenter", "com.color.safecenter.permission.PermissionManagerActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.color.safecenter",
|
||||
"com.color.safecenter.permission.PermissionManagerActivity"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoOppoSetting: 跳转OPPO权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoOppoSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LG经过测试,正常使用
|
||||
* @param activity
|
||||
* 跳转LG手机权限设置页
|
||||
*/
|
||||
public static void LG(Activity activity) {
|
||||
private static void gotoLgSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.android.settings", "com.android.settings.Settings$AccessLockSummaryActivity");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.android.settings",
|
||||
"com.android.settings.Settings$AccessLockSummaryActivity"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoLgSetting: 跳转LG权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoLgSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 乐视6.0以上很少,基本都可以忽略了,现在乐视手机不多
|
||||
* @param activity
|
||||
* 跳转乐视手机权限设置页
|
||||
*/
|
||||
public static void Letv(Activity activity) {
|
||||
private static void gotoLetvSetting(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent();
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.PermissionAndApps");
|
||||
intent.setComponent(comp);
|
||||
intent.setComponent(new ComponentName("com.letv.android.letvsafe",
|
||||
"com.letv.android.letvsafe.PermissionAndApps"));
|
||||
activity.startActivity(intent);
|
||||
isAppSettingOpen = false;
|
||||
LogUtils.d(TAG, "gotoLetvSetting: 跳转乐视权限设置页成功");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "gotoLetvSetting: 跳转失败,降级到应用详情页", e);
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 降级跳转方法区 ======================
|
||||
/**
|
||||
* 只能打开到自带安全软件
|
||||
* @param activity
|
||||
* 跳转系统设置主界面
|
||||
*/
|
||||
public static void _360(Activity activity) {
|
||||
try {
|
||||
Intent intent = new Intent("android.intent.action.MAIN");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("packageName", activity.getPackageName());
|
||||
ComponentName comp = new ComponentName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
|
||||
intent.setComponent(comp);
|
||||
activity.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
openAppDetailSetting(activity);
|
||||
//activity.startActivityForResult(getAppDetailSettingIntent(activity), PERMISSION_SETTING_FOR_RESULT);
|
||||
public static void gotoSystemConfig(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "gotoSystemConfig: Activity 为 null,无法跳转");
|
||||
return;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 系统设置界面
|
||||
* @param activity
|
||||
*/
|
||||
public static void SystemConfig(Activity activity) {
|
||||
Intent intent = new Intent(Settings.ACTION_SETTINGS);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
/**
|
||||
* 获取应用详情页面
|
||||
* @return
|
||||
*/
|
||||
private static Intent getAppDetailSettingIntent(Activity activity) {
|
||||
Intent localIntent = new Intent();
|
||||
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
//if (Build.VERSION.SDK_INT >= 9) {
|
||||
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
localIntent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||
/*} else if (Build.VERSION.SDK_INT <= 8) {
|
||||
localIntent.setAction(Intent.ACTION_VIEW);
|
||||
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
|
||||
localIntent.putExtra("com.android.settings.ApplicationPkgName", activity.getPackageName());
|
||||
}*/
|
||||
return localIntent;
|
||||
LogUtils.d(TAG, "gotoSystemConfig: 跳转系统设置主界面成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用详情页的 Intent
|
||||
*/
|
||||
private static Intent getAppDetailSettingIntent(Activity activity) {
|
||||
Intent intent = new Intent();
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开应用详情设置页
|
||||
*/
|
||||
public static void openAppDetailSetting(Activity activity) {
|
||||
if (activity == null) {
|
||||
LogUtils.e(TAG, "openAppDetailSetting: Activity 为 null,无法跳转");
|
||||
return;
|
||||
}
|
||||
activity.startActivityForResult(getAppDetailSettingIntent(activity), ACTIVITY_RESULT_APP_SETTINGS);
|
||||
isAppSettingOpen = true;
|
||||
LogUtils.d(TAG, "openAppDetailSetting: 跳转应用详情设置页成功");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人工具集
|
||||
*/
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
@@ -16,199 +11,341 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/08/30 14:32
|
||||
* @Describe 联系人工具集:提供联系人查询、添加、编辑、号码格式化等功能,适配主流机型
|
||||
*/
|
||||
public class ContactUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "ContactUtils";
|
||||
// 手机号正则(11位中国大陆手机号)
|
||||
private static final String REGEX_CHINA_MOBILE = "^1[0-9]{10}$";
|
||||
|
||||
Map<String, String> contactMap = new HashMap<>();
|
||||
// ====================== 单例与成员变量区 ======================
|
||||
// 单例实例(volatile 保证多线程可见性)
|
||||
private static volatile ContactUtils sInstance;
|
||||
// 上下文(弱引用避免内存泄漏,Java7 兼容)
|
||||
private final Context mContext;
|
||||
// 缓存联系人:key=纯数字号码,value=联系人姓名
|
||||
private final Map<String, String> mContactMap = new HashMap<>();
|
||||
|
||||
static volatile ContactUtils _ContactUtils;
|
||||
Context mContext;
|
||||
ContactUtils(Context context) {
|
||||
mContext = context;
|
||||
relaodContacts();
|
||||
// ====================== 单例构造区 ======================
|
||||
/**
|
||||
* 私有构造器:初始化上下文并加载联系人
|
||||
*/
|
||||
private ContactUtils(Context context) {
|
||||
// 传入应用上下文,避免Activity上下文泄漏
|
||||
this.mContext = context.getApplicationContext();
|
||||
LogUtils.d(TAG, "ContactUtils 初始化,开始加载联系人");
|
||||
reloadContacts();
|
||||
}
|
||||
public synchronized static ContactUtils getInstance(Context context) {
|
||||
if (_ContactUtils == null) {
|
||||
_ContactUtils = new ContactUtils(context);
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重校验锁,Java7 安全)
|
||||
*/
|
||||
public static ContactUtils getInstance(Context context) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "getInstance: 上下文为null,无法创建实例");
|
||||
throw new IllegalArgumentException("Context cannot be null");
|
||||
}
|
||||
return _ContactUtils;
|
||||
}
|
||||
|
||||
public void relaodContacts() {
|
||||
readContacts();
|
||||
}
|
||||
|
||||
private void readContacts() {
|
||||
contactMap.clear();
|
||||
ContentResolver contentResolver = mContext.getContentResolver();
|
||||
Cursor cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
null, null, null, null);
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
//Map<String, String> contactMap = new HashMap<>();
|
||||
contactMap.put(formatToSimplePhoneNumber(phoneNumber), displayName);
|
||||
if (sInstance == null) {
|
||||
synchronized (ContactUtils.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ContactUtils(context);
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
// 此时 contactList 就是存储联系人信息的 Map 列表
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public String getContactsName(String phone) {
|
||||
String result = contactMap.get(formatToSimplePhoneNumber(phone));
|
||||
return result == null ? "[NotInContacts]" : result;
|
||||
// ====================== 联系人缓存与查询区 ======================
|
||||
/**
|
||||
* 重新加载联系人到缓存
|
||||
*/
|
||||
public void reloadContacts() {
|
||||
LogUtils.d(TAG, "reloadContacts: 开始刷新联系人缓存");
|
||||
mContactMap.clear();
|
||||
readContactsFromSystem();
|
||||
LogUtils.d(TAG, "reloadContacts: 联系人缓存刷新完成,共缓存 " + mContactMap.size() + " 个联系人");
|
||||
}
|
||||
|
||||
// static String getSimplePhone(String phone) {
|
||||
// return phone.replaceAll("[+\\s]", "");
|
||||
// }
|
||||
/**
|
||||
* 从系统通讯录读取所有联系人(核心方法)
|
||||
*/
|
||||
private void readContactsFromSystem() {
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
// 只查询姓名和号码字段,减少IO开销
|
||||
String[] projection = {
|
||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||
};
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor == null) {
|
||||
LogUtils.w(TAG, "readContactsFromSystem: 通讯录查询Cursor为null,可能缺少权限");
|
||||
return;
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
|
||||
|
||||
if (phone != null) {
|
||||
String simplePhone = formatToSimplePhoneNumber(phone);
|
||||
mContactMap.put(simplePhone, name != null ? name : "[UnknownName]");
|
||||
LogUtils.v(TAG, "readContactsFromSystem: 缓存联系人 - 号码:" + simplePhone + ",姓名:" + name);
|
||||
}
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "readContactsFromSystem: 读取通讯录失败,缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "readContactsFromSystem: 读取通讯录异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 确保游标关闭,避免内存泄漏
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中获取联系人姓名
|
||||
*/
|
||||
public String getContactName(String phone) {
|
||||
if (phone == null) {
|
||||
LogUtils.w(TAG, "getContactName: 输入号码为null");
|
||||
return "[NotInContacts]";
|
||||
}
|
||||
String simplePhone = formatToSimplePhoneNumber(phone);
|
||||
String name = mContactMap.get(simplePhone);
|
||||
LogUtils.d(TAG, "getContactName: 查询号码 " + simplePhone + ",姓名:" + (name == null ? "[NotInContacts]" : name));
|
||||
return name == null ? "[NotInContacts]" : name;
|
||||
}
|
||||
|
||||
// ====================== 号码格式化工具区 ======================
|
||||
/**
|
||||
* 格式化号码为纯数字(去除所有非数字字符)
|
||||
*/
|
||||
public static String formatToSimplePhoneNumber(String number) {
|
||||
// 去除所有空格和非数字字符
|
||||
return number.replaceAll("[^0-9]", "");
|
||||
if (number == null || number.isEmpty()) {
|
||||
LogUtils.w(TAG, "formatToSimplePhoneNumber: 输入号码为空");
|
||||
return "";
|
||||
}
|
||||
String simpleNumber = number.replaceAll("[^0-9]", "");
|
||||
LogUtils.v(TAG, "formatToSimplePhoneNumber: 原号码 " + number + " → 纯数字号码 " + simpleNumber);
|
||||
return simpleNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化11位手机号为带空格格式(如:138 0000 1234)
|
||||
*/
|
||||
public static String formatToSpacePhoneNumber(String simpleNumber) {
|
||||
if (simpleNumber == null || !simpleNumber.matches(REGEX_CHINA_MOBILE)) {
|
||||
LogUtils.v(TAG, "formatToSpacePhoneNumber: 号码不符合11位手机号格式,无需格式化");
|
||||
return simpleNumber;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(simpleNumber.substring(0, 3))
|
||||
.append(" ")
|
||||
.append(simpleNumber.substring(3, 7))
|
||||
.append(" ")
|
||||
.append(simpleNumber.substring(7, 11));
|
||||
|
||||
String formatted = sb.toString();
|
||||
LogUtils.v(TAG, "formatToSpacePhoneNumber: 纯数字号码 " + simpleNumber + " → 带空格号码 " + formatted);
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// ====================== 联系人查询(直接查系统,不走缓存)区 ======================
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按原始号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhone(Context context, String phoneNumber) {
|
||||
String displayName = null;
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhone: 上下文或号码为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};
|
||||
Cursor cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, ContactsContract.CommonDataKinds.Phone.NUMBER + "=?", new String[]{phoneNumber}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
Cursor cursor = null;
|
||||
String displayName = null;
|
||||
|
||||
try {
|
||||
cursor = resolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER + "=?",
|
||||
new String[]{phoneNumber},
|
||||
null
|
||||
);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
}
|
||||
LogUtils.d(TAG, "getDisplayNameByPhone: 按原始号码 " + phoneNumber + " 查询,姓名:" + displayName);
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getDisplayNameByPhone: 缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getDisplayNameByPhone: 查询异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接查询系统通讯录获取联系人姓名(按纯数字号码匹配)
|
||||
*/
|
||||
public static String getDisplayNameByPhoneSimple(Context context, String phoneNumber) {
|
||||
String displayName = null;
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};
|
||||
Cursor cursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, ContactsContract.CommonDataKinds.Phone.NUMBER + "=?", new String[]{formatToSimplePhoneNumber(phoneNumber)}, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
|
||||
cursor.close();
|
||||
if (phoneNumber == null) {
|
||||
LogUtils.w(TAG, "getDisplayNameByPhoneSimple: 输入号码为null");
|
||||
return null;
|
||||
}
|
||||
return displayName;
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
LogUtils.d(TAG, "getDisplayNameByPhoneSimple: 按纯数字号码 " + simplePhone + " 查询");
|
||||
return getDisplayNameByPhone(context, simplePhone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断号码是否在系统通讯录中
|
||||
*/
|
||||
public static boolean isPhoneInContacts(Context context, String phoneNumber) {
|
||||
String szPhoneNumber = formatToSimplePhoneNumber(phoneNumber);
|
||||
String szDisplayName = getDisplayNameByPhone(context, szPhoneNumber);
|
||||
if (szDisplayName == null) {
|
||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", szPhoneNumber));
|
||||
szPhoneNumber = formatToSpacePhoneNumber(szPhoneNumber);
|
||||
szDisplayName = getDisplayNameByPhone(context, szPhoneNumber);
|
||||
if (szDisplayName == null) {
|
||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", szPhoneNumber));
|
||||
if (context == null || phoneNumber == null) {
|
||||
LogUtils.w(TAG, "isPhoneInContacts: 上下文或号码为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
String simplePhone = formatToSimplePhoneNumber(phoneNumber);
|
||||
String displayName = getDisplayNameByPhone(context, simplePhone);
|
||||
|
||||
if (displayName == null) {
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 未找到联系人(纯数字匹配)");
|
||||
String spacePhone = formatToSpacePhoneNumber(simplePhone);
|
||||
displayName = getDisplayNameByPhone(context, spacePhone);
|
||||
if (displayName == null) {
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + spacePhone + " 未找到联系人(带空格匹配)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, String.format("Phone %s is found in contacts %s.", szPhoneNumber, szDisplayName));
|
||||
|
||||
LogUtils.d(TAG, "isPhoneInContacts: 号码 " + simplePhone + " 已在联系人中,姓名:" + displayName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String formatToSpacePhoneNumber(String simpleNumber) {
|
||||
// 去除所有空格和非数字字符
|
||||
StringBuilder sbSpaceNumber = new StringBuilder();
|
||||
String regex = "^1[0-9]{10}$";
|
||||
if (simpleNumber.matches(regex)) {
|
||||
sbSpaceNumber.append(simpleNumber.substring(0, 3));
|
||||
sbSpaceNumber.append(" ");
|
||||
sbSpaceNumber.append(simpleNumber.substring(3, 7));
|
||||
sbSpaceNumber.append(" ");
|
||||
sbSpaceNumber.append(simpleNumber.substring(7, 11));
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配定制机型)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (context == null || phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.w(TAG, "getContactIdByPhone: 上下文或号码为空");
|
||||
return -1L;
|
||||
}
|
||||
return sbSpaceNumber.toString();
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
|
||||
Cursor cursor = null;
|
||||
Long contactId = -1L;
|
||||
|
||||
try {
|
||||
cursor = resolver.query(queryUri, projection, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
|
||||
}
|
||||
LogUtils.d(TAG, "getContactIdByPhone: 号码 " + phoneNumber + " 对应的联系人ID:" + contactId);
|
||||
} catch (SecurityException e) {
|
||||
LogUtils.e(TAG, "getContactIdByPhone: 缺少 READ_CONTACTS 权限", e);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "getContactIdByPhone: 查询异常", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return contactId;
|
||||
}
|
||||
|
||||
// ====================== 联系人跳转工具区 ======================
|
||||
/**
|
||||
* 跳转至系统添加联系人界面
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 预填号码(可为null)
|
||||
*/
|
||||
public static void jumpToAddContact(Context context, String phoneNumber) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToAddContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转至系统添加联系人界面的工具函数
|
||||
* @param context 上下文(如 PhoneCallService、Activity、Fragment 均可,需传入有效上下文)
|
||||
* @param phoneNumber 可选参数:预填的联系人电话(传 null 则跳转空表单)
|
||||
*/
|
||||
public static void jumpToAddContact(Context mContext, String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT);
|
||||
intent.setType("vnd.android.cursor.dir/person");
|
||||
intent.putExtra(android.provider.ContactsContract.Intents.Insert.PHONE, phoneNumber);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_INSERT);
|
||||
intent.setType("vnd.android.cursor.dir/person");
|
||||
if (phoneNumber != null) {
|
||||
intent.putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber);
|
||||
LogUtils.d(TAG, "jumpToAddContact: 跳转添加联系人,预填号码:" + phoneNumber);
|
||||
} else {
|
||||
LogUtils.d(TAG, "jumpToAddContact: 跳转添加联系人,无预填号码");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @param context 上下文(Activity/Service/Fragment)
|
||||
* @param phoneNumber 待编辑联系人的电话号码(用于匹配已有联系人,必传)
|
||||
* @param contactId 可选:已有联系人的ID(通过 ContactsContract 获取,传null则自动匹配号码)
|
||||
*/
|
||||
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
// 关键:小米等机型需明确设置数据类型为“单个联系人”,避免参数丢失
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 支持非Activity上下文调用
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
// 场景A:已知联系人ID(精准定位,优先用此方式,参数传递最稳定)
|
||||
if (contactId != null && contactId > 0) {
|
||||
// 构建联系人的Uri(格式:content://contacts/people/[contactId],系统标准格式)
|
||||
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
|
||||
intent.setData(contactUri);
|
||||
//ToastUtils.show("1");
|
||||
} else if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
// 方式1:小米等机型兼容的“通过号码定位联系人”参数(部分系统认此参数)
|
||||
//intent.putExtra(ContactsContract.Intents.Insert.PHONE_NUMBER, phoneNumber);
|
||||
// 方式2:补充系统标准的“数据Uri”,强化匹配(避免参数被定制系统忽略)
|
||||
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
intent.setData(phoneUri);
|
||||
} else {
|
||||
LogUtils.d(TAG, "编辑联系人失败:电话号码和联系人ID均为空");
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 跳转至系统编辑联系人界面(适配小米等定制机型)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待编辑号码(必传)
|
||||
* @param contactId 联系人ID(可选,优先使用)
|
||||
*/
|
||||
public static void jumpToEditContact(Context context, String phoneNumber, Long contactId) {
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 上下文为null");
|
||||
return;
|
||||
}
|
||||
|
||||
// 可选:预填最新号码(覆盖原有号码,若用户修改了号码,编辑时自动更新)
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
// 校验必要参数
|
||||
if (contactId == null || contactId <= 0) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
LogUtils.e(TAG, "jumpToEditContact: 联系人ID和号码均为空,无法编辑");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动活动(加防护,避免无联系人应用崩溃)
|
||||
// 小米机型在Service/非Activity中调用,需加NEW_TASK标志,否则可能无法启动
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
/**
|
||||
* 通过电话号码查询联系人ID(适配小米机型,解决编辑时匹配不稳定问题)
|
||||
* @param context 上下文
|
||||
* @param phoneNumber 待查询的电话号码
|
||||
* @return 联系人ID(无匹配时返回-1)
|
||||
*/
|
||||
public static Long getContactIdByPhone(Context context, String phoneNumber) {
|
||||
if (phoneNumber == null || phoneNumber.isEmpty()) {
|
||||
return -1L;
|
||||
}
|
||||
// 优先通过ID定位(精准)
|
||||
if (contactId != null && contactId > 0) {
|
||||
Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
|
||||
intent.setData(contactUri);
|
||||
LogUtils.d(TAG, "jumpToEditContact: 通过ID " + contactId + " 定位联系人,准备编辑");
|
||||
} else {
|
||||
// 通过号码定位
|
||||
Uri phoneUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
intent.setData(phoneUri);
|
||||
LogUtils.d(TAG, "jumpToEditContact: 通过号码 " + phoneNumber + " 定位联系人,准备编辑");
|
||||
}
|
||||
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
// 1. 构建电话查询Uri(系统标准:通过号码过滤联系人数据)
|
||||
Uri queryUri = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
|
||||
// 2. 只查询“联系人ID”字段(高效,避免冗余数据)
|
||||
String[] projection = {ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = cr.query(queryUri, projection, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
// 3. 读取联系人ID(返回Long类型,避免int溢出)
|
||||
return cursor.getLong(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.CONTACT_ID));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "查询联系人ID失败。" + e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close(); // 关闭游标,避免内存泄漏
|
||||
}
|
||||
}
|
||||
return -1L; // 无匹配联系人
|
||||
}
|
||||
// 预填最新号码
|
||||
if (phoneNumber != null && !phoneNumber.isEmpty()) {
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber);
|
||||
intent.putExtra(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
|
||||
}
|
||||
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import android.widget.EditText;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/04/13 00:59:13
|
||||
* @Describe Int类型数字输入框工具集
|
||||
* @Describe Int类型数字输入框工具集:安全读取 EditText 中的整数内容
|
||||
*/
|
||||
public class EditTextIntUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "EditTextIntUtils";
|
||||
// 默认返回值:读取失败时返回
|
||||
private static final int DEFAULT_INT_VALUE = 0;
|
||||
|
||||
// ====================== 工具方法区 ======================
|
||||
/**
|
||||
* 从 EditText 中安全读取整数
|
||||
* @param editText 目标输入框
|
||||
* @return 输入框中的整数,读取失败返回 0
|
||||
*/
|
||||
public static int getIntFromEditText(EditText editText) {
|
||||
// 空值校验:防止 EditText 为 null 导致空指针
|
||||
if (editText == null) {
|
||||
LogUtils.w(TAG, "getIntFromEditText: EditText 实例为 null,返回默认值 " + DEFAULT_INT_VALUE);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
|
||||
// 获取并去除首尾空格
|
||||
String inputStr = editText.getText().toString().trim();
|
||||
LogUtils.d(TAG, "getIntFromEditText: 输入框原始内容 | " + inputStr);
|
||||
|
||||
// 校验空字符串
|
||||
if (inputStr.isEmpty()) {
|
||||
LogUtils.w(TAG, "getIntFromEditText: 输入框内容为空,返回默认值 " + DEFAULT_INT_VALUE);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
|
||||
// 安全转换整数,捕获格式异常
|
||||
try {
|
||||
String sz = editText.getText().toString().trim();
|
||||
return Integer.parseInt(sz);
|
||||
int result = Integer.parseInt(inputStr);
|
||||
LogUtils.d(TAG, "getIntFromEditText: 转换成功 | 结果=" + result);
|
||||
return result;
|
||||
} catch (NumberFormatException e) {
|
||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
||||
return 0;
|
||||
LogUtils.e(TAG, "getIntFromEditText: 内容不是有效整数 | 输入内容=" + inputStr, e);
|
||||
return DEFAULT_INT_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,64 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/04/13 01:16:28
|
||||
* @Describe Int数字操作工具集
|
||||
* @Describe Int数字操作工具集:提供整数范围限制、数值边界校准功能
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class IntUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "IntUtils";
|
||||
|
||||
// ====================== 核心工具方法区 ======================
|
||||
/**
|
||||
* 将整数限制在指定区间内,自动校准超出边界的数值
|
||||
* @param origin 原始整数
|
||||
* @param range_a 区间端点1(无需区分大小)
|
||||
* @param range_b 区间端点2(无需区分大小)
|
||||
* @return 校准后的整数,结果始终在 [min(range_a,range_b), max(range_a,range_b)] 内
|
||||
*/
|
||||
public static int getIntInRange(int origin, int range_a, int range_b) {
|
||||
int min = Math.min(range_a, range_b);
|
||||
int max = Math.max(range_a, range_b);
|
||||
int res = Math.min(origin, max);
|
||||
res = Math.max(res, min);
|
||||
|
||||
// 打印调试日志,记录参数与计算结果
|
||||
LogUtils.d(TAG, String.format("getIntInRange: 原始值=%d, 区间=[%d,%d], 校准后=%d",
|
||||
origin, min, max, res));
|
||||
return res;
|
||||
}
|
||||
|
||||
// ====================== 单元测试方法区 ======================
|
||||
/**
|
||||
* 单元测试:验证 getIntInRange 方法在不同场景下的正确性
|
||||
*/
|
||||
public static void unittest_getIntInRange() {
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-100, 5, 10); %d", getIntInRange(-100, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(8, 5, 10); %d", getIntInRange(8, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(200, 5, 10); %d", getIntInRange(200, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-100, -5, 10); %d", getIntInRange(-100, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(9, -5, 10); %d", getIntInRange(9, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(100, -5, 10); %d", getIntInRange(100, -5, 10)));
|
||||
LogUtils.i(TAG, "unittest_getIntInRange: 开始执行单元测试");
|
||||
|
||||
LogUtils.d(TAG, String.format("getIntInRange(500, 5, -10); %d", getIntInRange(500, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(4, 5, -10); %d", getIntInRange(4, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(-20, 5, -10); %d", getIntInRange(-20, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(500, 50, 10); %d", getIntInRange(500, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(30, 50, 10); %d", getIntInRange(30, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("getIntInRange(6, 50, 10); %d", getIntInRange(6, 50, 10)));
|
||||
// 正数区间测试
|
||||
LogUtils.d(TAG, String.format("测试1: getIntInRange(-100, 5, 10) = %d", getIntInRange(-100, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试2: getIntInRange(8, 5, 10) = %d", getIntInRange(8, 5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试3: getIntInRange(200, 5, 10) = %d", getIntInRange(200, 5, 10)));
|
||||
|
||||
// 跨正负区间测试
|
||||
LogUtils.d(TAG, String.format("测试4: getIntInRange(-100, -5, 10) = %d", getIntInRange(-100, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试5: getIntInRange(9, -5, 10) = %d", getIntInRange(9, -5, 10)));
|
||||
LogUtils.d(TAG, String.format("测试6: getIntInRange(100, -5, 10) = %d", getIntInRange(100, -5, 10)));
|
||||
|
||||
// 端点顺序颠倒测试
|
||||
LogUtils.d(TAG, String.format("测试7: getIntInRange(500, 5, -10) = %d", getIntInRange(500, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("测试8: getIntInRange(4, 5, -10) = %d", getIntInRange(4, 5, -10)));
|
||||
LogUtils.d(TAG, String.format("测试9: getIntInRange(-20, 5, -10) = %d", getIntInRange(-20, 5, -10)));
|
||||
|
||||
// 大数区间测试
|
||||
LogUtils.d(TAG, String.format("测试10: getIntInRange(500, 50, 10) = %d", getIntInRange(500, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("测试11: getIntInRange(30, 50, 10) = %d", getIntInRange(30, 50, 10)));
|
||||
LogUtils.d(TAG, String.format("测试12: getIntInRange(6, 50, 10) = %d", getIntInRange(6, 50, 10)));
|
||||
|
||||
LogUtils.i(TAG, "unittest_getIntInRange: 单元测试执行完毕");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,58 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 15:21:48
|
||||
* @Describe PhoneUtils
|
||||
*/
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/02/26 15:21:48
|
||||
* @Describe 拨打电话工具类:封装拨打电话逻辑与权限校验
|
||||
*/
|
||||
public class PhoneUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "PhoneUtils";
|
||||
|
||||
// 拨打电话 Action 与 Uri 前缀
|
||||
private static final String CALL_ACTION = Intent.ACTION_CALL;
|
||||
private static final String TEL_URI_PREFIX = "tel:";
|
||||
|
||||
// ====================== 核心工具方法区 ======================
|
||||
/**
|
||||
* 直接拨打电话(需申请 CALL_PHONE 权限)
|
||||
* @param context 上下文对象
|
||||
* @param phoneNumber 目标电话号码
|
||||
*/
|
||||
public static void call(Context context, String phoneNumber) {
|
||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
// 空值校验:防止上下文或号码为空导致异常
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "call: Context 为 null,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
context.startActivity(intent);
|
||||
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "call: 电话号码为空,无法执行拨打电话操作");
|
||||
return;
|
||||
}
|
||||
String targetPhone = phoneNumber.trim();
|
||||
LogUtils.d(TAG, "call: 准备拨打号码 | " + targetPhone);
|
||||
|
||||
// 权限校验:检查是否持有拨打电话权限
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||
LogUtils.w(TAG, "call: 缺少 CALL_PHONE 权限,无法直接拨打电话");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建拨打电话 Intent 并启动
|
||||
Intent callIntent = new Intent(CALL_ACTION);
|
||||
callIntent.setData(Uri.parse(TEL_URI_PREFIX + targetPhone));
|
||||
// 添加 FLAG 支持非 Activity 上下文启动
|
||||
callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
context.startActivity(callIntent);
|
||||
LogUtils.i(TAG, "call: 拨打电话 Intent 已发送 | 号码=" + targetPhone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
package cc.winboll.studio.contacts.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe .* 前置预防针
|
||||
regex pointer preventive injection
|
||||
简称 RegexPPi
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2024/12/09 19:00:21
|
||||
* @Describe 正则前置校验工具类(RegexPPi):检验文本是否满足基础正则匹配要求
|
||||
*/
|
||||
public class RegexPPiUtils {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "RegexPPiUtils";
|
||||
// 基础匹配正则:匹配任意文本(包括空字符串)
|
||||
private static final String BASE_REGEX = ".*";
|
||||
// 预编译正则 Pattern,提升重复调用效率
|
||||
private static final Pattern BASE_PATTERN = Pattern.compile(BASE_REGEX);
|
||||
|
||||
//
|
||||
// 检验文本是否满足适合正则表达式模式计算
|
||||
//
|
||||
// ====================== 核心校验方法区 ======================
|
||||
/**
|
||||
* 检验文本是否满足基础正则表达式模式(.*)匹配要求
|
||||
* @param text 待校验的文本内容
|
||||
* @return 匹配结果,文本为null时返回false
|
||||
*/
|
||||
public static boolean isPPiOK(String text) {
|
||||
//String text = "这里是一些任意的文本内容";
|
||||
String regex = ".*";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
/*if (matcher.matches()) {
|
||||
System.out.println("文本满足该正则表达式模式");
|
||||
} else {
|
||||
System.out.println("文本不满足该正则表达式模式");
|
||||
}*/
|
||||
return matcher.matches();
|
||||
// 空值校验,避免空指针异常
|
||||
if (text == null) {
|
||||
LogUtils.w(TAG, "isPPiOK: 待校验文本为 null,返回 false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行正则匹配
|
||||
Matcher matcher = BASE_PATTERN.matcher(text);
|
||||
boolean isMatch = matcher.matches();
|
||||
|
||||
// 打印调试日志,记录校验结果
|
||||
LogUtils.d(TAG, String.format("isPPiOK: 文本=[%s],匹配结果=%b", text, isMatch));
|
||||
return isMatch;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,117 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/02 21:11:03
|
||||
* @Describe 云盾防御信息
|
||||
* @Describe 云盾防御信息视图控件:展示云盾防御值统计,并支持消息驱动更新
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||
import cc.winboll.studio.contacts.dun.Rules;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class DuInfoTextView extends TextView {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "DuInfoTextView";
|
||||
|
||||
public static final int MSG_NOTIFY_INFO_UPDATE = 0;
|
||||
|
||||
Context mContext;
|
||||
|
||||
public DuInfoTextView(android.content.Context context) {
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
private Context mContext;
|
||||
private Handler mHandler;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public DuInfoTextView(Context context) {
|
||||
super(context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public DuInfoTextView(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public DuInfoTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
void initView(android.content.Context context) {
|
||||
mContext = context;
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void initView(Context context) {
|
||||
LogUtils.d(TAG, "initView: 开始初始化云盾信息控件");
|
||||
this.mContext = context;
|
||||
initHandler();
|
||||
updateInfo();
|
||||
LogUtils.d(TAG, "initView: 云盾信息控件初始化完成");
|
||||
}
|
||||
|
||||
void updateInfo() {
|
||||
LogUtils.d(TAG, "updateInfo()");
|
||||
SettingsBean settingsModel = Rules.getInstance(mContext).getSettingsModel();
|
||||
String info = String.format("(云盾防御值【%d/%d】)", settingsModel.getDunCurrentCount(), settingsModel.getDunTotalCount());
|
||||
setText(info);
|
||||
}
|
||||
|
||||
Handler mHandler = new Handler(){
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if(msg.what == MSG_NOTIFY_INFO_UPDATE) {
|
||||
updateInfo();
|
||||
|
||||
/**
|
||||
* 初始化 Handler,处理信息更新消息
|
||||
*/
|
||||
private void initHandler() {
|
||||
mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
if (msg.what == MSG_NOTIFY_INFO_UPDATE) {
|
||||
LogUtils.d(TAG, "handleMessage: 收到信息更新消息,开始刷新视图");
|
||||
updateInfo();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ====================== 视图更新方法区 ======================
|
||||
/**
|
||||
* 更新云盾防御信息显示
|
||||
*/
|
||||
private void updateInfo() {
|
||||
LogUtils.d(TAG, "updateInfo: 开始更新云盾防御信息");
|
||||
// 空值校验,避免上下文为空导致异常
|
||||
if (mContext == null) {
|
||||
LogUtils.w(TAG, "updateInfo: 上下文为空,跳过信息更新");
|
||||
setText("(云盾防御值【--/--】)");
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
try {
|
||||
SettingsBean settingsModel = Rules.getInstance(mContext).getSettingsModel();
|
||||
// 校验 SettingsBean 非空,防止空指针
|
||||
if (settingsModel == null) {
|
||||
LogUtils.w(TAG, "updateInfo: SettingsBean 为空,显示默认值");
|
||||
setText("(云盾防御值【--/--】)");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentCount = settingsModel.getDunCurrentCount();
|
||||
int totalCount = settingsModel.getDunTotalCount();
|
||||
String info = String.format("(云盾防御值【%d/%d】)", currentCount, totalCount);
|
||||
setText(info);
|
||||
LogUtils.d(TAG, "updateInfo: 云盾防御信息更新完成 | " + info);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "updateInfo: 信息更新异常", e);
|
||||
setText("(云盾防御值【--/--】)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对外提供的信息更新通知方法
|
||||
*/
|
||||
public void notifyInfoUpdate() {
|
||||
LogUtils.d(TAG, "notifyInfoUpdate()");
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
LogUtils.d(TAG, "notifyInfoUpdate: 发送信息更新通知");
|
||||
if (mHandler != null) {
|
||||
mHandler.sendMessage(mHandler.obtainMessage(MSG_NOTIFY_INFO_UPDATE));
|
||||
} else {
|
||||
LogUtils.w(TAG, "notifyInfoUpdate: Handler 未初始化,无法发送更新消息");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.contacts.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2025/03/04 10:51:50
|
||||
* @Describe CustomHorizontalScrollView
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
@@ -16,10 +11,17 @@ import android.widget.TextView;
|
||||
import cc.winboll.studio.contacts.R;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/03/04 10:51:50
|
||||
* @Describe 左滑显示操作按钮的自定义滚动视图,支持编辑、删除、上移、下移功能
|
||||
*/
|
||||
public class LeftScrollView extends HorizontalScrollView {
|
||||
|
||||
// ====================== 常量定义区 ======================
|
||||
public static final String TAG = "LeftScrollView";
|
||||
|
||||
// ====================== 成员变量区 ======================
|
||||
// 布局控件
|
||||
private LinearLayout contentLayout;
|
||||
private LinearLayout toolLayout;
|
||||
private TextView textView;
|
||||
@@ -27,11 +29,15 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
private Button deleteButton;
|
||||
private Button upButton;
|
||||
private Button downButton;
|
||||
// 滑动事件相关
|
||||
private float mStartX;
|
||||
private float mEndX;
|
||||
private boolean isScrolling = false;
|
||||
private int nScrollAcceptSize;
|
||||
// 回调接口
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
// ====================== 构造函数区 ======================
|
||||
public LeftScrollView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
@@ -47,174 +53,254 @@ public class LeftScrollView extends HorizontalScrollView {
|
||||
init();
|
||||
}
|
||||
|
||||
public void addContentLayout(View viewContent) {
|
||||
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
// ====================== 初始化方法区 ======================
|
||||
private void init() {
|
||||
LogUtils.d(TAG, "init: 开始初始化左滑滚动视图");
|
||||
// 加载布局
|
||||
View viewMain = inflate(getContext(), R.layout.view_left_scroll, null);
|
||||
if (viewMain == null) {
|
||||
LogUtils.e(TAG, "init: 布局加载失败,无法初始化控件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定布局控件
|
||||
contentLayout = viewMain.findViewById(R.id.content_layout);
|
||||
toolLayout = viewMain.findViewById(R.id.action_layout);
|
||||
editButton = viewMain.findViewById(R.id.edit_btn);
|
||||
deleteButton = viewMain.findViewById(R.id.delete_btn);
|
||||
upButton = viewMain.findViewById(R.id.up_btn);
|
||||
downButton = viewMain.findViewById(R.id.down_btn);
|
||||
|
||||
// 校验控件是否绑定成功
|
||||
if (contentLayout == null || toolLayout == null) {
|
||||
LogUtils.e(TAG, "init: 核心布局控件绑定失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加主布局到当前视图
|
||||
addView(viewMain);
|
||||
// 设置按钮点击事件
|
||||
setButtonClickListener();
|
||||
|
||||
LogUtils.d(TAG, "init: 左滑滚动视图初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作按钮的点击事件
|
||||
*/
|
||||
private void setButtonClickListener() {
|
||||
// 编辑按钮
|
||||
if (editButton != null) {
|
||||
editButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击编辑按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onEdit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
if (deleteButton != null) {
|
||||
deleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击删除按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDelete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 上移按钮
|
||||
if (upButton != null) {
|
||||
upButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击上移按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 下移按钮
|
||||
if (downButton != null) {
|
||||
downButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "onClick: 点击下移按钮");
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ====================== 对外提供的方法区 ======================
|
||||
/**
|
||||
* 添加内容视图到容器
|
||||
* @param viewContent 待添加的内容视图
|
||||
*/
|
||||
public void addContentLayout(View viewContent) {
|
||||
if (contentLayout == null) {
|
||||
LogUtils.w(TAG, "addContentLayout: 内容布局未初始化,无法添加视图");
|
||||
return;
|
||||
}
|
||||
if (viewContent == null) {
|
||||
LogUtils.w(TAG, "addContentLayout: 待添加视图为null");
|
||||
return;
|
||||
}
|
||||
contentLayout.addView(viewContent, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
LogUtils.d(TAG, "addContentLayout: 内容视图添加成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内容布局的宽度
|
||||
* @param contentWidth 目标宽度
|
||||
*/
|
||||
public void setContentWidth(int contentWidth) {
|
||||
if (contentLayout == null) {
|
||||
LogUtils.w(TAG, "setContentWidth: 内容布局未初始化,无法设置宽度");
|
||||
return;
|
||||
}
|
||||
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) contentLayout.getLayoutParams();
|
||||
layoutParams.width = contentWidth;
|
||||
contentLayout.setLayoutParams(layoutParams);
|
||||
|
||||
LogUtils.d(TAG, "setContentWidth: 内容布局宽度设置为 " + contentWidth);
|
||||
}
|
||||
|
||||
private void init() {
|
||||
View viewMain = inflate(getContext(), R.layout.view_left_scroll, null);
|
||||
|
||||
// 创建内容布局
|
||||
contentLayout = viewMain.findViewById(R.id.content_layout);
|
||||
toolLayout = viewMain.findViewById(R.id.action_layout);
|
||||
|
||||
//LogUtils.d(TAG, String.format("getWidth() %d", getWidth()));
|
||||
|
||||
addView(viewMain);
|
||||
|
||||
// 创建编辑按钮
|
||||
editButton = viewMain.findViewById(R.id.edit_btn);
|
||||
// 创建删除按钮
|
||||
deleteButton = viewMain.findViewById(R.id.delete_btn);
|
||||
// 向上按钮
|
||||
upButton = viewMain.findViewById(R.id.up_btn);
|
||||
// 向下按钮
|
||||
downButton = viewMain.findViewById(R.id.down_btn);
|
||||
|
||||
// 编辑按钮点击事件
|
||||
editButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onEdit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击事件
|
||||
deleteButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDelete();
|
||||
}
|
||||
}
|
||||
});
|
||||
// 编辑按钮点击事件
|
||||
upButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onUp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮点击事件
|
||||
downButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onActionListener != null) {
|
||||
onActionListener.onDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 设置文本内容(原代码未初始化textView,添加空校验)
|
||||
* @param text 待显示的文本
|
||||
*/
|
||||
public void setText(CharSequence text) {
|
||||
if (textView == null) {
|
||||
LogUtils.w(TAG, "setText: 文本控件未初始化,无法设置文本");
|
||||
return;
|
||||
}
|
||||
textView.setText(text);
|
||||
LogUtils.d(TAG, "setText: 文本设置为 " + text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件回调监听器
|
||||
* @param listener 回调接口实例
|
||||
*/
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
LogUtils.d(TAG, "setOnActionListener: 事件监听器已设置");
|
||||
}
|
||||
|
||||
// ====================== 滑动事件处理区 ======================
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (event == null) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
LogUtils.d(TAG, "ACTION_DOWN");
|
||||
mStartX = event.getX();
|
||||
// isScrolling = false;
|
||||
LogUtils.d(TAG, "onTouchEvent: ACTION_DOWN,起始X坐标 = " + mStartX);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
//LogUtils.d(TAG, "ACTION_MOVE");
|
||||
// float currentX = event.getX();
|
||||
// float deltaX = mStartX - currentX;
|
||||
// //mLastX = currentX;
|
||||
// if (Math.abs(deltaX) > 0) {
|
||||
// isScrolling = true;
|
||||
// }
|
||||
// 可根据需求添加滑动中逻辑
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (getScrollX() > 0) {
|
||||
LogUtils.d(TAG, "ACTION_UP");
|
||||
mEndX = event.getX();
|
||||
LogUtils.d(TAG, String.format("mStartX %f, mEndX %f", mStartX, mEndX));
|
||||
if (mEndX < mStartX) {
|
||||
LogUtils.d(TAG, String.format("mEndX >= mStartX \ngetScrollX() %d", getScrollX()));
|
||||
//if (getScrollX() > editButton.getWidth()) {
|
||||
if (Math.abs(mStartX - mEndX) > editButton.getWidth()) {
|
||||
smoothScrollToRight();
|
||||
} else {
|
||||
smoothScrollToLeft();
|
||||
}
|
||||
} else {
|
||||
LogUtils.d(TAG, String.format("mEndX >= mStartX \ngetScrollX() %d", getScrollX()));
|
||||
//if (getScrollX() > deleteButton.getWidth()) {
|
||||
if (Math.abs(mEndX - mStartX) > deleteButton.getWidth()) {
|
||||
smoothScrollToLeft();
|
||||
} else {
|
||||
smoothScrollToRight();
|
||||
}
|
||||
}
|
||||
mEndX = event.getX();
|
||||
int scrollX = getScrollX();
|
||||
LogUtils.d(TAG, String.format("onTouchEvent: ACTION_UP/CANCEL,起始X=%f 结束X=%f 滚动距离=%d",
|
||||
mStartX, mEndX, scrollX));
|
||||
|
||||
if (scrollX > 0) {
|
||||
handleScrollLogic();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
void smoothScrollToRight() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
View childView = getChildAt(0);
|
||||
if (childView != null) {
|
||||
// 计算需要滑动到最右边的距离
|
||||
int scrollToX = childView.getWidth() - getWidth();
|
||||
// 确保滑动距离不小于0
|
||||
final int scrollToX2 = Math.max(0, scrollToX);
|
||||
// 平滑滑动到最右边
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(scrollToX2, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "smoothScrollTo(scrollToX, 0);");
|
||||
/**
|
||||
* 处理滑动结束后的逻辑,判断滑动方向并执行滚动
|
||||
*/
|
||||
private void handleScrollLogic() {
|
||||
float deltaX = Math.abs(mStartX - mEndX);
|
||||
// 校验按钮是否存在,避免空指针
|
||||
float threshold = editButton != null ? editButton.getWidth() : 50;
|
||||
|
||||
if (mEndX < mStartX) {
|
||||
// 向左滑,显示操作按钮
|
||||
if (deltaX > threshold) {
|
||||
smoothScrollToRight();
|
||||
} else {
|
||||
smoothScrollToLeft();
|
||||
}
|
||||
} else {
|
||||
// 向右滑,隐藏操作按钮
|
||||
if (deltaX > threshold) {
|
||||
smoothScrollToLeft();
|
||||
} else {
|
||||
smoothScrollToRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void smoothScrollToLeft() {
|
||||
mEndX = 0;
|
||||
mStartX = 0;
|
||||
// 在手指抬起时,使用 post 方法调用 smoothScrollTo(0, 0)
|
||||
/**
|
||||
* 平滑滚动到右侧(显示操作按钮)
|
||||
*/
|
||||
private void smoothScrollToRight() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollTo(0, 0);");
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void run() {
|
||||
View childView = getChildAt(0);
|
||||
if (childView != null) {
|
||||
int scrollToX = childView.getWidth() - getWidth();
|
||||
int targetX = Math.max(0, scrollToX);
|
||||
smoothScrollTo(targetX, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToRight: 滚动到右侧,目标X坐标 = " + targetX);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
// 设置文本内容
|
||||
public void setText(CharSequence text) {
|
||||
textView.setText(text);
|
||||
/**
|
||||
* 平滑滚动到左侧(隐藏操作按钮)
|
||||
*/
|
||||
private void smoothScrollToLeft() {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
smoothScrollTo(0, 0);
|
||||
LogUtils.d(TAG, "smoothScrollToLeft: 滚动到左侧");
|
||||
}
|
||||
});
|
||||
// 重置坐标
|
||||
resetScrollCoordinate();
|
||||
}
|
||||
|
||||
// 定义回调接口
|
||||
/**
|
||||
* 重置滑动坐标
|
||||
*/
|
||||
private void resetScrollCoordinate() {
|
||||
mStartX = 0;
|
||||
mEndX = 0;
|
||||
}
|
||||
|
||||
// ====================== 回调接口定义区 ======================
|
||||
public interface OnActionListener {
|
||||
void onEdit();
|
||||
void onDelete();
|
||||
void onUp();
|
||||
void onDown();
|
||||
}
|
||||
|
||||
private OnActionListener onActionListener;
|
||||
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.onActionListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user