diff --git a/contacts/build.properties b/contacts/build.properties index 85fd1b0..7718a05 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -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 diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java b/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java index 2f37ec0..6bba8da 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/ActivityStack.java @@ -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&豆包大模型 + * @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 mActivityList = new ArrayList(); - private List 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 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()); + } } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java index 20c16e4..19b56d8 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/MainActivity.java @@ -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() { diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java b/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java index 7d0e757..a8539a0 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/adapters/CallLogAdapter.java @@ -53,7 +53,7 @@ public class CallLogAdapter extends RecyclerView.Adapter + * @Date 2025/02/13 06:58:04 + * @Describe 接打电话界面,仅支持 Android 6.0(API 23)及以上系统 */ -@RequiresApi(api = Build.VERSION_CODES.M) +// Java7不支持@RequiresApi,通过代码内校验替代注解 public class PhoneCallActivity extends AppCompatActivity implements View.OnClickListener { + // ====================== 常量定义区 ====================== + public static final String TAG = "PhoneCallActivity"; + // Intent传参键常量,替代系统常量避免歧义 + private static final String EXTRA_CALL_TYPE = "extra_call_type"; + private static final String EXTRA_PHONE_NUM = "extra_phone_number"; + // ====================== 控件与成员变量区 ====================== private TextView tvCallNumberLabel; private TextView tvCallNumber; private TextView tvPickUp; @@ -36,126 +39,254 @@ public class PhoneCallActivity extends AppCompatActivity implements View.OnClick private PhoneCallManager phoneCallManager; private PhoneCallService.CallType callType; private String phoneNumber; - private Timer onGoingCallTimer; private int callingTime; - public static void actionStart(Context context, String phoneNumber, - PhoneCallService.CallType callType) { + // ====================== 静态跳转方法区 ====================== + /** + * 启动通话界面 + * @param context 上下文 + * @param phoneNumber 通话号码 + * @param callType 通话类型(来电/去电) + */ + public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) { + if (context == null) { + LogUtils.e(TAG, "actionStart: 上下文为null,无法启动通话界面"); + return; + } + if (phoneNumber == null || phoneNumber.isEmpty()) { + LogUtils.w(TAG, "actionStart: 通话号码为空"); + } + if (callType == null) { + LogUtils.w(TAG, "actionStart: 通话类型为null"); + } + Intent intent = new Intent(context, PhoneCallActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_MIME_TYPES, callType); - intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber); + intent.putExtra(EXTRA_CALL_TYPE, callType); + intent.putExtra(EXTRA_PHONE_NUM, phoneNumber); context.startActivity(intent); + LogUtils.d(TAG, "actionStart: 启动通话界面,号码=" + phoneNumber + ",类型=" + callType); } + // ====================== 生命周期方法区 ====================== @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_phone_call); + LogUtils.d(TAG, "onCreate: 通话界面创建"); + // Java7不支持@RequiresApi,此处补充系统版本校验 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + LogUtils.e(TAG, "onCreate: 系统版本低于Android 6.0,不支持该通话界面"); + finish(); + return; + } + + setContentView(R.layout.activity_phone_call); ActivityStack.getInstance().addActivity(this); initData(); initView(); - } + @Override + protected void onDestroy() { + super.onDestroy(); + LogUtils.d(TAG, "onDestroy: 通话界面销毁"); + stopTimer(); + if (phoneCallManager != null) { + phoneCallManager.destroy(); + } + ActivityStack.getInstance().removeActivity(this); + } + + // ====================== 初始化方法区 ====================== + /** + * 初始化数据 + */ private void initData() { phoneCallManager = new PhoneCallManager(this); onGoingCallTimer = new Timer(); - if (getIntent() != null) { - phoneNumber = getIntent().getStringExtra(Intent.EXTRA_PHONE_NUMBER); - callType = (PhoneCallService.CallType) getIntent().getSerializableExtra(Intent.EXTRA_MIME_TYPES); - } + parseIntentData(); + LogUtils.d(TAG, "initData: 数据初始化完成,通话类型=" + callType + ",号码=" + phoneNumber); } + /** + * 解析Intent传递的数据 + */ + private void parseIntentData() { + Intent intent = getIntent(); + if (intent == null) { + LogUtils.w(TAG, "parseIntentData: 未获取到启动Intent"); + return; + } + phoneNumber = intent.getStringExtra(EXTRA_PHONE_NUM); + callType = (PhoneCallService.CallType) intent.getSerializableExtra(EXTRA_CALL_TYPE); + } + + /** + * 初始化界面控件 + */ + @SuppressLint("SetTextI18n") private void initView() { + // 设置沉浸式导航栏 int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION //hide navigationBar - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; getWindow().getDecorView().setSystemUiVisibility(uiOptions); getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + LogUtils.d(TAG, "initView: 设置界面沉浸式样式"); + // 绑定控件 tvCallNumberLabel = findViewById(R.id.tv_call_number_label); tvCallNumber = findViewById(R.id.tv_call_number); tvPickUp = findViewById(R.id.tv_phone_pick_up); tvCallingTime = findViewById(R.id.tv_phone_calling_time); tvHangUp = findViewById(R.id.tv_phone_hang_up); - tvCallNumber.setText(formatPhoneNumber(phoneNumber)); + // 设置控件事件与初始状态 + tvCallNumber.setText(CallListenerService.formatPhoneNumber(phoneNumber)); tvPickUp.setOnClickListener(this); tvHangUp.setOnClickListener(this); - // 打进的电话 - if (callType == PhoneCallService.CallType.CALL_IN) { - tvCallNumberLabel.setText("来电号码"); - tvPickUp.setVisibility(View.VISIBLE); - } else if (callType == PhoneCallService.CallType.CALL_OUT) { - tvCallNumberLabel.setText("呼叫号码"); + setCallTypeUi(); + showOnLockScreen(); + LogUtils.d(TAG, "initView: 界面控件初始化完成"); + } + + // ====================== 界面交互方法区 ====================== + /** + * 根据通话类型设置界面样式 + */ + private void setCallTypeUi() { + if (callType == null) { + LogUtils.w(TAG, "setCallTypeUi: 通话类型为null,使用默认样式"); + tvCallNumberLabel.setText("通话号码"); tvPickUp.setVisibility(View.GONE); - phoneCallManager.openSpeaker(); + return; } - showOnLockScreen(); + if (PhoneCallService.CallType.CALL_IN == callType) { + tvCallNumberLabel.setText("来电号码"); + tvPickUp.setVisibility(View.VISIBLE); + } else if (PhoneCallService.CallType.CALL_OUT == callType) { + tvCallNumberLabel.setText("呼叫号码"); + tvPickUp.setVisibility(View.GONE); + if (phoneCallManager != null) { + phoneCallManager.openSpeaker(); + LogUtils.d(TAG, "setCallTypeUi: 去电模式,开启扬声器"); + } + } } - public void showOnLockScreen() { - this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | - WindowManager.LayoutParams.FLAG_FULLSCREEN | - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON, - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | - WindowManager.LayoutParams.FLAG_FULLSCREEN | - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + /** + * 设置界面锁屏显示 + */ + private void showOnLockScreen() { + getWindow().setFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON, + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ); + LogUtils.d(TAG, "showOnLockScreen: 设置界面支持锁屏显示"); } + // ====================== 事件点击与计时方法区 ====================== @Override public void onClick(View v) { - if (v.getId() == R.id.tv_phone_pick_up) { + if (v == null) { + LogUtils.w(TAG, "onClick: 点击空控件"); + return; + } + + int id = v.getId(); + if (id == R.id.tv_phone_pick_up) { + LogUtils.d(TAG, "onClick: 点击接听按钮"); + answerCall(); + } else if (id == R.id.tv_phone_hang_up) { + LogUtils.d(TAG, "onClick: 点击挂断按钮"); + hangUpCall(); + } else { + LogUtils.w(TAG, "onClick: 点击未知控件,ID=" + id); + } + } + + /** + * 接听通话 + */ + private void answerCall() { + if (phoneCallManager != null) { phoneCallManager.answer(); tvPickUp.setVisibility(View.GONE); tvCallingTime.setVisibility(View.VISIBLE); - onGoingCallTimer.schedule(new TimerTask() { - @Override - public void run() { - runOnUiThread(new Runnable() { - @SuppressLint("SetTextI18n") - @Override - public void run() { - callingTime++; - tvCallingTime.setText("通话中:" + getCallingTime()); - } - }); - } - }, 0, 1000); - } else if (v.getId() == R.id.tv_phone_hang_up) { - phoneCallManager.disconnect(); - stopTimer(); + startCallTimer(); + LogUtils.d(TAG, "answerCall: 接听通话成功"); + } else { + LogUtils.e(TAG, "answerCall: 通话管理器为空,无法接听"); } } - private String getCallingTime() { - int minute = callingTime / 60; - int second = callingTime % 60; - return (minute < 10 ? "0" + minute : minute) + - ":" + - (second < 10 ? "0" + second : second); + /** + * 挂断通话 + */ + private void hangUpCall() { + if (phoneCallManager != null) { + phoneCallManager.disconnect(); + } else { + LogUtils.e(TAG, "hangUpCall: 通话管理器为空,无法挂断"); + } + stopTimer(); + finish(); } + /** + * 启动通话计时器 + */ + private void startCallTimer() { + stopTimer(); + onGoingCallTimer = new Timer(); + onGoingCallTimer.schedule(new TimerTask() { + @Override + public void run() { + runOnUiThread(new Runnable() { + @SuppressLint("SetTextI18n") + @Override + public void run() { + callingTime++; + tvCallingTime.setText("通话中:" + getCallingTime()); + } + }); + } + }, 0, 1000); + LogUtils.d(TAG, "startCallTimer: 通话计时器启动"); + } + + /** + * 停止通话计时器 + */ private void stopTimer() { if (onGoingCallTimer != null) { onGoingCallTimer.cancel(); + onGoingCallTimer = null; } - callingTime = 0; + LogUtils.d(TAG, "stopTimer: 通话计时器停止"); } - @Override - protected void onDestroy() { - super.onDestroy(); - //MainActivity.updateCallLogFragment(); - phoneCallManager.destroy(); + /** + * 格式化通话时长 + */ + private String getCallingTime() { + int minute = callingTime / 60; + int second = callingTime % 60; + String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute); + String secondStr = second < 10 ? "0" + second : String.valueOf(second); + return minuteStr + ":" + secondStr; } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java index 70d7484..e872026 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallManager.java @@ -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&豆包大模型 + * @Date 2025/02/13 06:58:04 + * @Describe 通话管理工具类,负责接听、挂断通话及免提控制,仅支持Android 6.0(API 23)及以上系统 + */ public class PhoneCallManager { + // ====================== 常量定义区 ====================== + private static final String TAG = "PhoneCallManager"; + // 音频通话模式常量,与VideoProfile.STATE_AUDIO_ONLY保持一致,增强可读性 + private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY; - public static Call call; - - private Context context; - private AudioManager audioManager; + // ====================== 成员变量区 ====================== + // 静态通话实例,用于全局关联当前通话 + public static Call sCurrentCall; + // 上下文对象 + private Context mContext; + // 音频管理器,用于控制免提等音频相关操作 + private AudioManager mAudioManager; + // ====================== 构造方法区 ====================== + /** + * 构造通话管理实例 + * @param context 上下文 + */ public PhoneCallManager(Context context) { - this.context = context; - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + this.mContext = context; + initAudioManager(); + LogUtils.d(TAG, "PhoneCallManager: 通话管理工具初始化"); } + // ====================== 初始化辅助方法区 ====================== /** - * 接听电话 + * 初始化音频管理器 + */ + private void initAudioManager() { + if (mContext == null) { + LogUtils.e(TAG, "initAudioManager: 上下文为空,无法初始化音频管理器"); + return; + } + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + if (mAudioManager == null) { + LogUtils.e(TAG, "initAudioManager: 获取音频管理器失败"); + } else { + LogUtils.d(TAG, "initAudioManager: 音频管理器初始化成功"); + } + } + + // ====================== 通话核心操作方法区 ====================== + /** + * 接听当前通话 */ public void answer() { - if (call != null) { - call.answer(VideoProfile.STATE_AUDIO_ONLY); + // 校验系统版本,Android6.0以下不支持telecom.Call的answer方法 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + LogUtils.e(TAG, "answer: 系统版本低于Android6.0,不支持接听操作"); + return; + } + + if (sCurrentCall == null) { + LogUtils.w(TAG, "answer: 当前无通话实例,无法接听"); + return; + } + + try { + sCurrentCall.answer(VIDEO_PROFILE_AUDIO_ONLY); openSpeaker(); + LogUtils.d(TAG, "answer: 成功执行接听操作,通话模式为纯音频"); + } catch (SecurityException e) { + LogUtils.e(TAG, "answer: 接听通话权限不足", e); + } catch (IllegalStateException e) { + LogUtils.e(TAG, "answer: 通话状态异常,无法接听", e); } } /** - * 断开电话,包括来电时的拒接以及接听后的挂断 + * 断开当前通话(拒接或挂断) */ public void disconnect() { - if (call != null) { - call.disconnect(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + LogUtils.e(TAG, "disconnect: 系统版本低于Android6.0,不支持挂断操作"); + return; + } + + if (sCurrentCall == null) { + LogUtils.w(TAG, "disconnect: 当前无通话实例,无法挂断"); + return; + } + + try { + sCurrentCall.disconnect(); + LogUtils.d(TAG, "disconnect: 成功执行挂断操作"); + } catch (SecurityException e) { + LogUtils.e(TAG, "disconnect: 挂断通话权限不足", e); + } catch (IllegalStateException e) { + LogUtils.e(TAG, "disconnect: 通话状态异常,无法挂断", e); } } /** - * 打开免提 + * 打开通话免提 */ public void openSpeaker() { - if (audioManager != null) { - audioManager.setMode(AudioManager.MODE_IN_CALL); - audioManager.setSpeakerphoneOn(true); + if (mAudioManager == null) { + LogUtils.w(TAG, "openSpeaker: 音频管理器为空,无法打开免提"); + return; + } + + try { + mAudioManager.setMode(AudioManager.MODE_IN_CALL); + mAudioManager.setSpeakerphoneOn(true); + LogUtils.d(TAG, "openSpeaker: 免提功能已开启"); + } catch (IllegalStateException e) { + LogUtils.e(TAG, "openSpeaker: 音频模式设置失败,无法开启免提", e); } } + // ====================== 资源释放方法区 ====================== /** - * 销毁资源 + * 销毁资源,释放引用 */ public void destroy() { - call = null; - context = null; - audioManager = null; + // 关闭免提后再释放资源,避免音频状态异常 + if (mAudioManager != null) { + mAudioManager.setSpeakerphoneOn(false); + mAudioManager.setMode(AudioManager.MODE_NORMAL); + LogUtils.d(TAG, "destroy: 音频状态已恢复默认"); + } + sCurrentCall = null; + mAudioManager = null; + mContext = null; + LogUtils.d(TAG, "destroy: 通话管理工具资源已释放"); } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java index 0db920d..54c6101 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/phonecallui/PhoneCallService.java @@ -1,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&豆包大模型 + * @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 "未知状态"; + } } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java b/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java index 474ccc6..e135fa5 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/receivers/MainReceiver.java @@ -12,61 +12,87 @@ import java.lang.ref.WeakReference; /** * @Author ZhanGSKen&豆包大模型 * @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 mwrService; + // 使用弱引用关联 MainService,避免内存泄漏 + private WeakReference mMainServiceWeakRef; // ====================== 构造函数区 ====================== public MainReceiver(MainService service) { - this.mwrService = new WeakReference(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); + } } } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java index a402dcf..3145728 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/AssistantService.java @@ -1,10 +1,5 @@ package cc.winboll.studio.contacts.services; -/** - * @Author ZhanGSKen - * @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&豆包大模型 + * @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); } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java index b89a77f..ce0dfde 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/MainService.java @@ -1,15 +1,5 @@ package cc.winboll.studio.contacts.services; -/** - * @Author ZhanGSKen - * @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&豆包大模型 + * @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: 广播接收器已注册"); + } } } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/threads/MainServiceThread.java b/contacts/src/main/java/cc/winboll/studio/contacts/threads/MainServiceThread.java index ab30735..5f0e01b 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/threads/MainServiceThread.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/threads/MainServiceThread.java @@ -1,73 +1,104 @@ package cc.winboll.studio.contacts.threads; -/** - * @Author ZhanGSKen - * @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&豆包大模型 + * @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 mwrMainServiceHandler; + // 线程休眠周期(1秒) + private static final long THREAD_SLEEP_INTERVAL = 1000L; - MainServiceThread(Context context, MainServiceHandler handler) { - mContext = context; - mwrMainServiceHandler = new WeakReference(handler); + // ====================== 静态成员变量区 ====================== + private static volatile MainServiceThread sInstance; + + // ====================== 成员变量区 ====================== + // 线程运行控制标记 + private volatile boolean mIsExit; + private volatile boolean mIsStarted; + // 弱引用持有上下文和Handler,避免内存泄漏 + private WeakReference mContextWeakRef; + private WeakReference 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: 线程正常退出"); + } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppGoToSettingsUtil.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppGoToSettingsUtil.java index 38d8f0c..839b342 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppGoToSettingsUtil.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/AppGoToSettingsUtil.java @@ -1,270 +1,268 @@ package cc.winboll.studio.contacts.utils; -/** - * @Author ZhanGSKen&豆包大模型 - * @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&豆包大模型 + * @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: 跳转应用详情设置页成功"); } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java index 99483c1..9d2fc6c 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/ContactUtils.java @@ -1,10 +1,5 @@ package cc.winboll.studio.contacts.utils; -/** - * @Author ZhanGSKen&豆包大模型 - * @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&豆包大模型 + * @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 contactMap = new HashMap<>(); + // ====================== 单例与成员变量区 ====================== + // 单例实例(volatile 保证多线程可见性) + private static volatile ContactUtils sInstance; + // 上下文(弱引用避免内存泄漏,Java7 兼容) + private final Context mContext; + // 缓存联系人:key=纯数字号码,value=联系人姓名 + private final Map 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 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); + } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/EditTextIntUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/EditTextIntUtils.java index e6a9529..b0a44d5 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/EditTextIntUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/EditTextIntUtils.java @@ -1,24 +1,51 @@ package cc.winboll.studio.contacts.utils; + import android.widget.EditText; import cc.winboll.studio.libappbase.LogUtils; /** - * @Author ZhanGSKen + * @Author ZhanGSKen&豆包大模型 * @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; } } - } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/IntUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/IntUtils.java index e218e12..a5564c8 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/IntUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/IntUtils.java @@ -1,37 +1,64 @@ package cc.winboll.studio.contacts.utils; +import cc.winboll.studio.libappbase.LogUtils; + /** * @Author ZhanGSKen * @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: 单元测试执行完毕"); } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/PhoneUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/PhoneUtils.java index 4e5250e..34641de 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/PhoneUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/PhoneUtils.java @@ -1,27 +1,58 @@ package cc.winboll.studio.contacts.utils; -/** - * @Author ZhanGSKen - * @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&豆包大模型 + * @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); } - } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/utils/RegexPPiUtils.java b/contacts/src/main/java/cc/winboll/studio/contacts/utils/RegexPPiUtils.java index 98fc976..1f91c60 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/utils/RegexPPiUtils.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/utils/RegexPPiUtils.java @@ -1,32 +1,42 @@ package cc.winboll.studio.contacts.utils; -/** - * @Author ZhanGSKen - * @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&豆包大模型 + * @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; } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/views/DuInfoTextView.java b/contacts/src/main/java/cc/winboll/studio/contacts/views/DuInfoTextView.java index 35562aa..3b4a40d 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/views/DuInfoTextView.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/views/DuInfoTextView.java @@ -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 * @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 未初始化,无法发送更新消息"); + } } } + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/views/LeftScrollView.java b/contacts/src/main/java/cc/winboll/studio/contacts/views/LeftScrollView.java index fb13ae3..0cbc2b1 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/views/LeftScrollView.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/views/LeftScrollView.java @@ -1,10 +1,5 @@ package cc.winboll.studio.contacts.views; -/** - * @Author ZhanGSKen - * @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&豆包大模型 + * @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; - } }