源码整理

This commit is contained in:
2025-12-12 14:47:13 +08:00
parent 2dafa7bf9f
commit 63d365b175
19 changed files with 2223 additions and 1220 deletions

View File

@@ -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

View File

@@ -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());
}
}
}

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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.0API 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;
}
}

View File

@@ -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.0API 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: 通话管理工具资源已释放");
}
}

View File

@@ -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 "未知状态";
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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: 广播接收器已注册");
}
}
}

View File

@@ -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: 线程正常退出");
}
}

View File

@@ -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: 跳转应用详情设置页成功");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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: 单元测试执行完毕");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 未初始化,无法发送更新消息");
}
}
}

View File

@@ -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;
}
}