From 9a873bf162ea51f1126627a27eb6abc4673f6d42 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 13 Dec 2025 15:03:18 +0800 Subject: [PATCH] 20251213_150313_142 --- contacts/build.properties | 4 +- contacts/src/main/AndroidManifest.xml | 26 +- .../contacts/activities/SettingsActivity.java | 614 +++++++++++------- .../listenphonecall/CallListenerService.java | 561 +++++++++------- .../contacts/services/AssistantService.java | 8 +- .../studio/contacts/services/MainService.java | 538 ++++++++------- .../services/MyCallScreeningService.java | 337 +++++----- 7 files changed, 1228 insertions(+), 860 deletions(-) diff --git a/contacts/build.properties b/contacts/build.properties index 9a88b1f..321059e 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Dec 13 05:41:14 GMT 2025 +#Sat Dec 13 07:01:07 GMT 2025 stageCount=1 libraryProject= baseVersion=15.12 publishVersion=15.12.0 -buildCount=99 +buildCount=111 baseBetaVersion=15.12.1 diff --git a/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml index fffec35..174e272 100644 --- a/contacts/src/main/AndroidManifest.xml +++ b/contacts/src/main/AndroidManifest.xml @@ -98,19 +98,22 @@ + android:exported="false" + android:stopWithTask="false"/> + android:exported="false" + android:stopWithTask="false"/> + android:exported="false" + android:stopWithTask="false"> + android:stopWithTask="false"> @@ -137,15 +140,16 @@ + android:permission="android.permission.BIND_CALL_SCREENING_SERVICE" + android:exported="true" + android:stopWithTask="false"> - + @@ -153,7 +157,8 @@ + android:exported="true" + android:stopWithTask="false"> @@ -167,7 +172,8 @@ - + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java index d6601f7..4ca030b 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/activities/SettingsActivity.java @@ -41,39 +41,38 @@ import java.util.List; * @Author ZhanGSKen&豆包大模型 * @Date 2025/02/21 05:37:42 * @Describe Contacts 设置页面(完全适配 API 30 + Java 7 语法) - * 核心优化:1. 移除高版本API依赖 2. Java7规范写法 3. 强化内存泄漏防护 4. 版本判断硬编码 + * 核心优化:1. 移除高版本API依赖 2. Java7规范写法 3. 强化内存泄漏防护 4. 版本判断硬编码 5. LogUtils统一日志管理 */ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivity { - // ====================== 常量定义区 ====================== + // ====================== 常量定义区(置顶,统一管理) ====================== public static final String TAG = "SettingsActivity"; - // API版本硬编码常量(替代Build.VERSION_CODES,适配API30) + // API版本硬编码(替代Build.VERSION_CODES,适配Java7) private static final int ANDROID_6_API = 23; - // ====================== 静态控件区 ====================== - static DuInfoTextView _DuInfoTextView; + // ====================== 静态成员属性区 ====================== + private static DuInfoTextView sDuInfoTextView; // 规范命名:静态属性加s前缀 - // ====================== UI控件区 ====================== - private Toolbar mToolbar; - private Switch swSilent; - private SeekBar msbVolume; - private TextView mtvVolume; - private Switch mswMainService; - private EditText etDunTotalCount; - private EditText etDunResumeSecondCount; - private EditText etDunResumeCount; - private Switch swIsEnableDun; - private RecyclerView recyclerView; - private EditText etBoBullToonURL; - private EditText etPhone; + // ====================== 数据业务属性区 ====================== + private int mStreamMaxVolume; // 铃音最大音量 + private int mStreamVolume; // 当前铃音音量 + private List mRuleList; // 通话规则列表 + private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器 - // ====================== 数据与业务参数区 ====================== - private int mnStreamMaxVolume; - private int mnStreamVolume; - private PhoneConnectRuleAdapter adapter; - private List ruleList; + // ====================== UI控件属性区(统一归类,规范命名) ====================== + private Toolbar mToolbar; // 顶部工具栏 + private Switch mSwMainService; // 主服务开关 + private SeekBar mSbVolume; // 音量调节条 + private TextView mTvVolume; // 音量显示文本 + private Switch mSwEnableDun; // 云盾功能开关 + private EditText mEtDunTotalCount; // 云盾总次数输入框 + private EditText mEtDunResumeSecondCount; // 云盾恢复秒数输入框 + private EditText mEtDunResumeCount; // 云盾恢复次数输入框 + private RecyclerView mRvRuleList; // 规则列表RecyclerView + private EditText mEtBoBullToonUrl; // BoBullToon地址输入框 + private EditText mEtSearchPhone; // 号码查询输入框 - // ====================== 接口实现区 ====================== + // ====================== 接口实现区(IWinBoLLActivity规范实现) ====================== @Override public AppCompatActivity getActivity() { return this; @@ -84,25 +83,20 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit return TAG; } - // ====================== 生命周期函数区 ====================== + // ====================== 生命周期函数区(按执行顺序排列) ====================== @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - LogUtils.d(TAG, "onCreate: 设置页面开始创建"); + LogUtils.d(TAG, "onCreate: 设置页面启动"); setContentView(R.layout.activity_settings); - // 初始化工具栏 - initToolbar(); - // 初始化主服务开关 - initMainServiceSwitch(); - // 初始化铃声音量调节 - initVolumeSeekBar(); - // 初始化规则列表 - initRuleRecyclerView(); - // 初始化云盾设置 - initDunSettings(); - // 初始化BoBullToon相关控件 - initBoBullToonViews(); + // 初始化核心流程(按优先级执行) + initToolbar(); // 工具栏初始化(优先) + initMainServiceSwitch();// 主服务开关初始化 + initVolumeControl(); // 音量控制初始化 + initRuleRecyclerView(); // 规则列表初始化 + initDunSettings(); // 云盾设置初始化 + initBoBullToonViews(); // BoBullToon功能初始化 LogUtils.d(TAG, "onCreate: 设置页面初始化完成"); } @@ -110,56 +104,70 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit @Override protected void onDestroy() { super.onDestroy(); - LogUtils.d(TAG, "onDestroy: 设置页面开始销毁"); - // 清空静态引用 + 置空所有控件引用,彻底避免内存泄漏 - _DuInfoTextView = null; + LogUtils.d(TAG, "onDestroy: 设置页面销毁"); + // 内存泄漏防护:清空所有引用(静态+成员+UI) + sDuInfoTextView = null; + mRuleList = null; + mRuleAdapter = null; mToolbar = null; - mswMainService = null; - msbVolume = null; - mtvVolume = null; - recyclerView = null; - adapter = null; - ruleList = null; - LogUtils.d(TAG, "onDestroy: 设置页面销毁完成"); + mSwMainService = null; + mSbVolume = null; + mTvVolume = null; + mSwEnableDun = null; + mEtDunTotalCount = null; + mEtDunResumeSecondCount = null; + mEtDunResumeCount = null; + mRvRuleList = null; + mEtBoBullToonUrl = null; + mEtSearchPhone = null; + LogUtils.d(TAG, "onDestroy: 设置页面资源清理完成"); } - // ====================== 控件初始化函数区 ====================== + // ====================== 初始化函数区(按功能模块归类) ====================== + /** + * 初始化顶部工具栏(后退按钮+标题) + */ private void initToolbar() { LogUtils.d(TAG, "initToolbar: 初始化工具栏"); mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1); setSupportActionBar(mToolbar); - // 显示后退按钮(添加空指针防护) + + // 显示后退按钮(空指针防护) if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setSubtitle(getTag()); + getSupportActionBar().setSubtitle(TAG); } - // Java7 匿名内部类实现点击监听,禁止Lambda + + // 后退按钮点击事件(Java7匿名内部类) mToolbar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - LogUtils.d(TAG, "initToolbar: 点击导航栏返回按钮"); + LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面"); finish(); } }); } + /** + * 初始化主服务开关(联动MainService启停) + */ private void initMainServiceSwitch() { LogUtils.d(TAG, "initMainServiceSwitch: 初始化主服务开关"); - mswMainService = (Switch) findViewById(R.id.sw_mainservice); - MainServiceBean mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); - // 空指针防护:避免MainServiceBean为null导致崩溃 - if (mMainServiceBean != null) { - mswMainService.setChecked(mMainServiceBean.isEnable()); - } else { - mswMainService.setChecked(false); - LogUtils.w(TAG, "initMainServiceSwitch: MainServiceBean为null,默认关闭开关"); - } + mSwMainService = (Switch) findViewById(R.id.sw_mainservice); + MainServiceBean serviceBean = MainServiceBean.loadBean(this, MainServiceBean.class); - mswMainService.setOnClickListener(new View.OnClickListener() { + // 加载开关状态(空指针防护) + boolean isServiceEnable = serviceBean != null && serviceBean.isEnable(); + mSwMainService.setChecked(isServiceEnable); + LogUtils.d(TAG, "initMainServiceSwitch: 主服务当前状态:" + (isServiceEnable ? "启用" : "禁用")); + + // 开关点击事件 + mSwMainService.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View arg0) { - LogUtils.d(TAG, "initMainServiceSwitch: 主服务开关状态变更,checked=" + mswMainService.isChecked()); - if (mswMainService.isChecked()) { + public void onClick(View v) { + boolean isChecked = mSwMainService.isChecked(); + LogUtils.d(TAG, "initMainServiceSwitch: 主服务开关切换:" + (isChecked ? "启用" : "禁用")); + if (isChecked) { MainService.startMainServiceAndSaveStatus(SettingsActivity.this); } else { MainService.stopMainServiceAndSaveStatus(SettingsActivity.this); @@ -168,40 +176,44 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit }); } - private void initVolumeSeekBar() { - LogUtils.d(TAG, "initVolumeSeekBar: 初始化音量调节条"); - msbVolume = (SeekBar) findViewById(R.id.bellvolume); - mtvVolume = (TextView) findViewById(R.id.tv_volume); + /** + * 初始化音量控制(SeekBar+音量显示+配置保存) + */ + private void initVolumeControl() { + LogUtils.d(TAG, "initVolumeControl: 初始化音量控制"); + mSbVolume = (SeekBar) findViewById(R.id.bellvolume); + mTvVolume = (TextView) findViewById(R.id.tv_volume); final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - // 空指针防护:AudioManager可能为null + // 空指针防护:AudioManager获取失败直接返回 if (audioManager == null) { - LogUtils.e(TAG, "initVolumeSeekBar: AudioManager获取失败,跳过音量初始化"); + LogUtils.e(TAG, "initVolumeControl: AudioManager获取失败,音量控制初始化失败"); return; } - // 设置SeekBar最大值和初始进度 - mnStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING); - msbVolume.setMax(mnStreamMaxVolume); - mnStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); - msbVolume.setProgress(mnStreamVolume); - updateStreamVolumeTextView(); + // 初始化音量参数 + mStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING); + mStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); + mSbVolume.setMax(mStreamMaxVolume); + mSbVolume.setProgress(mStreamVolume); + updateVolumeDisplay(); // 更新音量文本显示 - // 音量调节监听(Java7匿名内部类) - msbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + // 音量调节监听 + mSbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { - LogUtils.d(TAG, "initVolumeSeekBar: 音量调节至" + progress); + LogUtils.d(TAG, "initVolumeControl: 音量调节至:" + progress + "/" + mStreamMaxVolume); + // 实时更新系统音量+保存配置 audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0); - RingTongBean bean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class); - if (bean == null) { - bean = new RingTongBean(); + RingTongBean ringBean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class); + if (ringBean == null) { + ringBean = new RingTongBean(); } - bean.setStreamVolume(progress); - RingTongBean.saveBean(SettingsActivity.this, bean); - mnStreamVolume = progress; - updateStreamVolumeTextView(); + ringBean.setStreamVolume(progress); + RingTongBean.saveBean(SettingsActivity.this, ringBean); + mStreamVolume = progress; + updateVolumeDisplay(); } } @@ -213,253 +225,340 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit }); } + /** + * 初始化通话规则列表(加载黑白名单规则) + */ private void initRuleRecyclerView() { LogUtils.d(TAG, "initRuleRecyclerView: 初始化规则列表"); - recyclerView = (RecyclerView) findViewById(R.id.recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - ruleList = Rules.getInstance(this).getPhoneBlacRuleBeanList(); - adapter = new PhoneConnectRuleAdapter(this, ruleList); - recyclerView.setAdapter(adapter); - LogUtils.d(TAG, "initRuleRecyclerView: 规则列表加载完成,共" + ruleList.size() + "条规则"); + mRvRuleList = (RecyclerView) findViewById(R.id.recycler_view); + mRvRuleList.setLayoutManager(new LinearLayoutManager(this)); + + // 加载规则数据 + Rules rules = Rules.getInstance(this); + if (rules == null) { + LogUtils.e(TAG, "initRuleRecyclerView: Rules实例获取失败,列表初始化失败"); + return; + } + mRuleList = rules.getPhoneBlacRuleBeanList(); + mRuleAdapter = new PhoneConnectRuleAdapter(this, mRuleList); + mRvRuleList.setAdapter(mRuleAdapter); + LogUtils.d(TAG, "initRuleRecyclerView: 规则列表加载完成,共" + mRuleList.size() + "条规则"); } + /** + * 初始化云盾设置(参数加载+开关联动) + */ private void initDunSettings() { LogUtils.d(TAG, "initDunSettings: 初始化云盾设置"); - _DuInfoTextView = (DuInfoTextView) findViewById(R.id.tv_DunInfo); - etDunTotalCount = (EditText) findViewById(R.id.et_DunTotalCount); - etDunResumeSecondCount = (EditText) findViewById(R.id.et_DunResumeSecondCount); - etDunResumeCount = (EditText) findViewById(R.id.et_DunResumeCount); - swIsEnableDun = (Switch) findViewById(R.id.sw_IsEnableDun); + sDuInfoTextView = (DuInfoTextView) findViewById(R.id.tv_DunInfo); + mSwEnableDun = (Switch) findViewById(R.id.sw_IsEnableDun); + mEtDunTotalCount = (EditText) findViewById(R.id.et_DunTotalCount); + mEtDunResumeSecondCount = (EditText) findViewById(R.id.et_DunResumeSecondCount); + mEtDunResumeCount = (EditText) findViewById(R.id.et_DunResumeCount); - SettingsBean settingsModel = Rules.getInstance(this).getSettingsModel(); - // 空指针防护:避免SettingsModel为null - if (settingsModel != null) { - etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount())); - etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount())); - etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount())); - swIsEnableDun.setChecked(settingsModel.isEnableDun()); - - boolean isEnableDun = settingsModel.isEnableDun(); - etDunTotalCount.setEnabled(!isEnableDun); - etDunResumeSecondCount.setEnabled(!isEnableDun); - etDunResumeCount.setEnabled(!isEnableDun); - } else { - LogUtils.e(TAG, "initDunSettings: SettingsModel为null,云盾设置初始化失败"); - } - } - - private void initBoBullToonViews() { - LogUtils.d(TAG, "initBoBullToonViews: 初始化BoBullToon相关控件"); - etBoBullToonURL = (EditText) findViewById(R.id.bobulltoonurl_et); - etPhone = (EditText) findViewById(R.id.activitysettingsEditText1); - // 空指针防护:避免Rules.getInstance返回null + // 加载云盾配置 Rules rules = Rules.getInstance(this); - if (rules != null) { - etBoBullToonURL.setText(rules.getBoBullToonURL()); + if (rules == null) { + LogUtils.e(TAG, "initDunSettings: Rules实例获取失败,云盾初始化失败"); + return; } - } - - // ====================== 工具函数区 ====================== - private void updateStreamVolumeTextView() { - // Java7 字符串拼接,避免String.format可能的空指针 - mtvVolume.setText(mnStreamVolume + "/" + mnStreamMaxVolume); - } - - // ====================== 静态通知函数区 ====================== - public static void notifyDunInfoUpdate() { - if (_DuInfoTextView != null) { - LogUtils.d(TAG, "notifyDunInfoUpdate: 更新云盾信息展示"); - _DuInfoTextView.notifyInfoUpdate(); - } else { - LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件为空,跳过更新"); - } - } - - // ====================== 点击事件回调函数区 ====================== - public void onSW_IsEnableDun(View view) { - LogUtils.d(TAG, "onSW_IsEnableDun: 云盾开关状态变更,checked=" + swIsEnableDun.isChecked()); - boolean isEnableDun = swIsEnableDun.isChecked(); - etDunTotalCount.setEnabled(!isEnableDun); - etDunResumeSecondCount.setEnabled(!isEnableDun); - etDunResumeCount.setEnabled(!isEnableDun); - - SettingsBean settingsModel = Rules.getInstance(this).getSettingsModel(); - // 空指针防护:SettingsModel为null时直接返回 - if (settingsModel == null) { - LogUtils.e(TAG, "onSW_IsEnableDun: SettingsModel为null,操作失败"); - swIsEnableDun.setChecked(false); + SettingsBean dunSettings = rules.getSettingsModel(); + if (dunSettings == null) { + LogUtils.e(TAG, "initDunSettings: 云盾配置获取失败"); return; } - if (isEnableDun) { + // 填充配置参数 + mEtDunTotalCount.setText(String.valueOf(dunSettings.getDunTotalCount())); + mEtDunResumeSecondCount.setText(String.valueOf(dunSettings.getDunResumeSecondCount())); + mEtDunResumeCount.setText(String.valueOf(dunSettings.getDunResumeCount())); + mSwEnableDun.setChecked(dunSettings.isEnableDun()); + + // 开关联动:启用云盾时禁用参数编辑 + boolean isDunEnable = dunSettings.isEnableDun(); + mEtDunTotalCount.setEnabled(!isDunEnable); + mEtDunResumeSecondCount.setEnabled(!isDunEnable); + mEtDunResumeCount.setEnabled(!isDunEnable); + LogUtils.d(TAG, "initDunSettings: 云盾当前状态:" + (isDunEnable ? "启用" : "禁用")); + } + + /** + * 初始化BoBullToon功能(地址配置+号码查询) + */ + private void initBoBullToonViews() { + LogUtils.d(TAG, "initBoBullToonViews: 初始化BoBullToon功能"); + mEtBoBullToonUrl = (EditText) findViewById(R.id.bobulltoonurl_et); + mEtSearchPhone = (EditText) findViewById(R.id.activitysettingsEditText1); + + // 加载保存的地址 + Rules rules = Rules.getInstance(this); + if (rules != null) { + mEtBoBullToonUrl.setText(rules.getBoBullToonURL()); + LogUtils.d(TAG, "initBoBullToonViews: 加载BoBullToon地址完成"); + } else { + LogUtils.e(TAG, "initBoBullToonViews: Rules实例获取失败,地址加载失败"); + } + } + + // ====================== 点击事件回调区(按功能模块归类) ====================== + /** + * 云盾开关点击事件(联动参数编辑权限+配置保存) + */ + public void onSW_IsEnableDun(View view) { + boolean isChecked = mSwEnableDun.isChecked(); + LogUtils.d(TAG, "onSW_IsEnableDun: 云盾开关切换:" + (isChecked ? "启用" : "禁用")); + + // 联动参数编辑权限 + mEtDunTotalCount.setEnabled(!isChecked); + mEtDunResumeSecondCount.setEnabled(!isChecked); + mEtDunResumeCount.setEnabled(!isChecked); + + // 保存配置 + Rules rules = Rules.getInstance(this); + if (rules == null) { + LogUtils.e(TAG, "onSW_IsEnableDun: Rules实例获取失败,配置保存失败"); + mSwEnableDun.setChecked(false); + return; + } + SettingsBean dunSettings = rules.getSettingsModel(); + if (dunSettings == null) { + LogUtils.e(TAG, "onSW_IsEnableDun: 云盾配置获取失败,保存失败"); + mSwEnableDun.setChecked(false); + return; + } + + // 启用云盾时校验参数合法性 + if (isChecked) { try { - String totalCountStr = etDunTotalCount.getText().toString().trim(); - String resumeSecondStr = etDunResumeSecondCount.getText().toString().trim(); - String resumeCountStr = etDunResumeCount.getText().toString().trim(); - // 空字符串校验 - if (totalCountStr.isEmpty() || resumeSecondStr.isEmpty() || resumeCountStr.isEmpty()) { + String totalCountStr = mEtDunTotalCount.getText().toString().trim(); + String resumeSecStr = mEtDunResumeSecondCount.getText().toString().trim(); + String resumeCountStr = mEtDunResumeCount.getText().toString().trim(); + + // 空参数校验 + if (totalCountStr.isEmpty() || resumeSecStr.isEmpty() || resumeCountStr.isEmpty()) { throw new NumberFormatException("参数不能为空"); } + + // 转换参数并保存 int totalCount = Integer.parseInt(totalCountStr); - int resumeSecond = Integer.parseInt(resumeSecondStr); + int resumeSec = Integer.parseInt(resumeSecStr); int resumeCount = Integer.parseInt(resumeCountStr); - settingsModel.setDunTotalCount(totalCount); - settingsModel.setDunResumeSecondCount(resumeSecond); - settingsModel.setDunResumeCount(resumeCount); - // Java7 条件判断 + 字符串拼接 - String toastMsg = (totalCount == 1) ? "电话骚扰防御力几乎为0。" : "以下设置将在连拨" + totalCount + "次后接通电话。"; + dunSettings.setDunTotalCount(totalCount); + dunSettings.setDunResumeSecondCount(resumeSec); + dunSettings.setDunResumeCount(resumeCount); + LogUtils.d(TAG, "onSW_IsEnableDun: 云盾参数保存完成,总次数:" + totalCount + ",恢复秒数:" + resumeSec); + + // 提示信息 + String toastMsg = totalCount == 1 ? "电话骚扰防御力几乎为0" : "连拨" + totalCount + "次后接通电话"; ToastUtils.show(toastMsg); - LogUtils.d(TAG, "onSW_IsEnableDun: 云盾参数更新完成,totalCount=" + totalCount); } catch (NumberFormatException e) { LogUtils.e(TAG, "onSW_IsEnableDun: 云盾参数格式错误", e); ToastUtils.show("参数格式错误,请输入整数"); - swIsEnableDun.setChecked(false); + mSwEnableDun.setChecked(false); return; } } - settingsModel.setIsEnableDun(isEnableDun); - Rules.getInstance(this).saveDun(); - Rules.getInstance(this).reload(); - // 刷新参数显示 - etDunTotalCount.setText(Integer.toString(settingsModel.getDunTotalCount())); - etDunResumeSecondCount.setText(Integer.toString(settingsModel.getDunResumeSecondCount())); - etDunResumeCount.setText(Integer.toString(settingsModel.getDunResumeCount())); - } - - public void onUnitTest(View view) { - LogUtils.d(TAG, "onUnitTest: 点击进入单元测试页面"); - Intent intent = new Intent(this, UnitTestActivity.class); - startActivity(intent); + // 保存开关状态并刷新配置 + dunSettings.setIsEnableDun(isChecked); + rules.saveDun(); + rules.reload(); + LogUtils.d(TAG, "onSW_IsEnableDun: 云盾配置保存完成"); } + /** + * 添加新通话规则(黑白名单) + */ public void onAddNewConnectionRule(View view) { - LogUtils.d(TAG, "onAddNewConnectionRule: 添加新的连接规则"); + LogUtils.d(TAG, "onAddNewConnectionRule: 添加新通话规则"); Rules rules = Rules.getInstance(this); - if (rules != null) { - rules.getPhoneBlacRuleBeanList().add(new PhoneConnectRuleBean()); - rules.saveRules(); - adapter.notifyDataSetChanged(); - } else { - LogUtils.e(TAG, "onAddNewConnectionRule: Rules实例为null,添加规则失败"); + if (rules == null) { + LogUtils.e(TAG, "onAddNewConnectionRule: Rules实例获取失败,添加失败"); + return; } + mRuleList.add(new PhoneConnectRuleBean()); + rules.saveRules(); + mRuleAdapter.notifyDataSetChanged(); + LogUtils.d(TAG, "onAddNewConnectionRule: 规则添加完成,当前共" + mRuleList.size() + "条规则"); } + /** + * 跳转默认电话应用设置 + */ public void onDefaultPhone(View view) { - LogUtils.d(TAG, "onDefaultPhone: 跳转到默认电话应用设置"); - Intent intent = new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS); - startActivity(intent); + LogUtils.d(TAG, "onDefaultPhone: 跳转默认电话应用设置"); + startActivity(new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)); } + /** + * 悬浮窗权限检查与请求 + */ public void onCanDrawOverlays(View view) { LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限"); - // 版本判断硬编码(替代Build.VERSION_CODES.M),适配API30 + // API6.0+校验权限 if (Build.VERSION.SDK_INT >= ANDROID_6_API && !Settings.canDrawOverlays(this)) { - askForDrawOverlay(); + LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求"); + showDrawOverlayRequestDialog(); } else { - ToastUtils.show("悬浮窗已开启"); + ToastUtils.show("悬浮窗权限已开启"); } } + /** + * 清理BoBullToon本地数据 + */ public void onCleanBoBullToonData(View view) { LogUtils.d(TAG, "onCleanBoBullToonData: 清理BoBullToon数据"); TomCat tomCat = TomCat.getInstance(this); if (tomCat != null) { tomCat.cleanBoBullToon(); ToastUtils.show("BoBullToon数据已清理"); + LogUtils.d(TAG, "onCleanBoBullToonData: 数据清理完成"); + } else { + LogUtils.e(TAG, "onCleanBoBullToonData: TomCat实例获取失败,清理失败"); } } + /** + * 重置BoBullToon默认地址 + */ public void onResetBoBullToonURL(View view) { - LogUtils.d(TAG, "onResetBoBullToonURL: 重置BoBullToon URL"); + LogUtils.d(TAG, "onResetBoBullToonURL: 重置BoBullToon地址"); Rules rules = Rules.getInstance(this); - if (rules != null) { - rules.resetDefaultBoBullToonURL(); - etBoBullToonURL.setText(rules.getBoBullToonURL()); - ToastUtils.show("已重置 BoBullToon URL。"); + if (rules == null) { + LogUtils.e(TAG, "onResetBoBullToonURL: Rules实例获取失败,重置失败"); + return; } + rules.resetDefaultBoBullToonURL(); + mEtBoBullToonUrl.setText(rules.getBoBullToonURL()); + ToastUtils.show("BoBullToon地址已重置为默认"); + LogUtils.d(TAG, "onResetBoBullToonURL: 地址重置完成"); } + /** + * 下载BoBullToon数据(子线程执行,避免阻塞UI) + */ public void onDownloadBoBullToon(View view) { LogUtils.d(TAG, "onDownloadBoBullToon: 开始下载BoBullToon数据"); Rules rules = Rules.getInstance(this); if (rules == null) { - LogUtils.e(TAG, "onDownloadBoBullToon: Rules实例为null,下载失败"); + LogUtils.e(TAG, "onDownloadBoBullToon: Rules实例获取失败,下载失败"); return; } - String inputUrl = etBoBullToonURL.getText().toString().trim(); + + // 校验并更新地址 + String inputUrl = mEtBoBullToonUrl.getText().toString().trim(); String savedUrl = rules.getBoBullToonURL(); if (!inputUrl.equals(savedUrl)) { rules.setBoBullToonURL(inputUrl); - LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon URL已更新为" + inputUrl); + LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon地址更新为:" + inputUrl); } + // 子线程下载(Java7匿名内部类) final TomCat tomCat = TomCat.getInstance(this); - // Java7 匿名内部类实现Runnable,禁止Lambda new Thread(new Runnable() { @Override public void run() { - if (tomCat != null && tomCat.downloadBoBullToon()) { - LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon 下载成功"); - // 主线程更新UI(Java7 匿名内部类) + boolean downloadSuccess = tomCat != null && tomCat.downloadBoBullToon(); + if (downloadSuccess) { + LogUtils.d(TAG, "onDownloadBoBullToon: 数据下载成功"); + // 主线程更新UI runOnUiThread(new Runnable() { @Override public void run() { - ToastUtils.show("BoBullToon download OK!"); + ToastUtils.show("BoBullToon下载成功"); } }); + // 重启主服务+刷新配置 MainService.restartMainService(SettingsActivity.this); Rules.getInstance(SettingsActivity.this).reload(); } else { - LogUtils.e(TAG, "onDownloadBoBullToon: BoBullToon 下载失败"); + LogUtils.e(TAG, "onDownloadBoBullToon: 数据下载失败"); + runOnUiThread(new Runnable() { + @Override + public void run() { + ToastUtils.show("BoBullToon下载失败"); + } + }); } } }).start(); } + /** + * 查询号码是否为BoBullToon号码 + */ public void onSearchBoBullToonPhone(View view) { - LogUtils.d(TAG, "onSearchBoBullToonPhone: 搜索BoBullToon号码"); - TomCat tomCat = TomCat.getInstance(this); - String phone = etPhone.getText().toString().trim(); + LogUtils.d(TAG, "onSearchBoBullToonPhone: 执行号码查询"); + String phone = mEtSearchPhone.getText().toString().trim(); + // 空号码校验 if (phone.isEmpty()) { - ToastUtils.show("请输入要查询的号码"); + LogUtils.w(TAG, "onSearchBoBullToonPhone: 查询号码为空,取消查询"); + ToastUtils.show("请输入查询号码"); return; } - if (tomCat != null && tomCat.loadPhoneBoBullToon()) { - boolean isBoBullToon = tomCat.isPhoneBoBullToon(phone); - String toastMsg = isBoBullToon ? "It is a BoBullToon Phone!" : "Not in BoBullToon."; - ToastUtils.show(toastMsg); - LogUtils.d(TAG, "onSearchBoBullToonPhone: 号码" + phone + "查询结果:" + (isBoBullToon ? "是" : "否") + "为BoBullToon号码"); - } else { - ToastUtils.show("没有下载 BoBullToon。"); + + // 执行查询 + TomCat tomCat = TomCat.getInstance(this); + if (tomCat == null || !tomCat.loadPhoneBoBullToon()) { + LogUtils.w(TAG, "onSearchBoBullToonPhone: BoBullToon数据未加载,查询失败"); + ToastUtils.show("请先下载BoBullToon数据"); + return; } + + boolean isBoBullToon = tomCat.isPhoneBoBullToon(phone); + String resultMsg = isBoBullToon ? "是BoBullToon号码" : "非BoBullToon号码"; + ToastUtils.show(resultMsg); + LogUtils.d(TAG, "onSearchBoBullToonPhone: 号码" + phone + "查询结果:" + resultMsg); } + /** + * 跳转单元测试页面 + */ + public void onUnitTest(View view) { + LogUtils.d(TAG, "onUnitTest: 跳转单元测试页面"); + startActivity(new Intent(this, UnitTestActivity.class)); + } + + /** + * 跳转关于页面 + */ public void onAbout(View view) { - LogUtils.d(TAG, "onAbout: 跳转到关于页面"); + LogUtils.d(TAG, "onAbout: 跳转关于页面"); WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class); } + /** + * 跳转日志查看页面 + */ public void onLogView(View view) { - LogUtils.d(TAG, "onLogView: 跳转到日志页面"); + LogUtils.d(TAG, "onLogView: 跳转日志页面"); WinBoLLActivityManager.getInstance().startLogActivity(this); } - // ====================== 悬浮窗权限请求函数区 ====================== - private void askForDrawOverlay() { - LogUtils.d(TAG, "askForDrawOverlay: 弹出悬浮窗权限请求对话框"); - // Java7 匿名内部类实现Dialog监听 - AlertDialog alertDialog = new AlertDialog.Builder(this) - .setTitle("允许显示悬浮框") - .setMessage("为了使电话监听服务正常工作,请允许这项权限") + // ====================== 工具方法区(通用功能+权限相关) ====================== + /** + * 更新音量显示文本(当前音量/最大音量) + */ + private void updateVolumeDisplay() { + mTvVolume.setText(mStreamVolume + "/" + mStreamMaxVolume); + } + + /** + * 显示悬浮窗权限请求对话框 + */ + private void showDrawOverlayRequestDialog() { + AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle("权限请求") + .setMessage("为保证通话监听功能正常,需开启悬浮窗权限") .setPositiveButton("去设置", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - openDrawOverlaySettings(); dialog.dismiss(); + jumpToDrawOverlaySettings(); } }) - .setNegativeButton("稍后再说", new DialogInterface.OnClickListener() { + .setNegativeButton("稍后", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); @@ -467,30 +566,47 @@ public class SettingsActivity extends WinBollActivity implements IWinBoLLActivit }) .create(); - if (alertDialog.getWindow() != null) { - alertDialog.getWindow().setFlags( + // 解决对话框焦点问题 + if (dialog.getWindow() != null) { + dialog.getWindow().setFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); } - alertDialog.show(); + dialog.show(); } - private void openDrawOverlaySettings() { - LogUtils.d(TAG, "openDrawOverlaySettings: 跳转悬浮窗管理设置界面"); - // 版本判断硬编码 - if (Build.VERSION.SDK_INT >= ANDROID_6_API) { - try { - Context context = this; - Class clazz = Settings.class; - Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION"); - Intent intent = new Intent(field.get(null).toString()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setData(Uri.parse("package:" + context.getPackageName())); - context.startActivity(intent); - } catch (Exception e) { - LogUtils.e(TAG, "openDrawOverlaySettings: 跳转悬浮窗设置失败", e); - Toast.makeText(this, "请在悬浮窗管理中打开权限", Toast.LENGTH_LONG).show(); - } + /** + * 跳转悬浮窗权限设置页面(反射适配低版本) + */ + private void jumpToDrawOverlaySettings() { + LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置"); + try { + // 反射获取设置页面Action(避免高版本API依赖) + Class settingsClazz = Settings.class; + Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION"); + String action = (String) actionField.get(null); + + // 跳转当前应用权限设置页 + Intent intent = new Intent(action); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + } catch (Exception e) { + LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e); + Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show(); + } + } + + // ====================== 静态通知方法区(云盾信息更新) ====================== + /** + * 通知云盾信息刷新(外部调用) + */ + public static void notifyDunInfoUpdate() { + if (sDuInfoTextView != null) { + LogUtils.d(TAG, "notifyDunInfoUpdate: 刷新云盾信息显示"); + sDuInfoTextView.notifyInfoUpdate(); + } else { + LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件未初始化,刷新失败"); } } } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java b/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java index f36a8e7..9230eb4 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/listenphonecall/CallListenerService.java @@ -1,8 +1,5 @@ package cc.winboll.studio.contacts.listenphonecall; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -25,253 +22,371 @@ import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; import cc.winboll.studio.contacts.R; +import cc.winboll.studio.contacts.model.MainServiceBean; import cc.winboll.studio.contacts.phonecallui.PhoneCallActivity; import cc.winboll.studio.contacts.phonecallui.PhoneCallService; import cc.winboll.studio.libappbase.LogUtils; /** - * 终极优化版:onCreate 仅注册前台服务,其他逻辑延迟初始化,避免阻塞 - * 适配 Java7 + Android API30 + * @Author ZhanGSKen&豆包大模型 + * @Describe 通话监听服务(无前台服务),负责监听通话状态、显示通话悬浮窗、跳转通话界面 + * 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 避免内存泄漏 */ public class CallListenerService extends Service { - private static final String TAG = "CallListenerService"; - private static final String CHANNEL_ID = "call_listener_channel"; - private static final int NOTIFICATION_ID = 1003; - private static final int FOREGROUND_SERVICE_TYPE_PHONE_CALL = 0x80; - private static final int ANDROID_8_API = 26; - private static final int ANDROID_10_API = 29; - private static final int ANDROID_19_API = 19; - // View 与服务相关属性 - private View phoneCallView; - private TextView tvCallNumber; - private Button btnOpenApp; - private WindowManager windowManager; - private WindowManager.LayoutParams params; - private PhoneStateListener phoneStateListener; - private TelephonyManager telephonyManager; - private String callNumber; - private boolean hasShown; - private boolean isCallingIn; - // 延迟初始化 Handler - private Handler mDelayHandler; + // ====================== 常量定义区(精准适配API29-30,无冗余) ====================== + public static final String TAG = "CallListenerService"; + // Android版本常量(仅保留适配必需版本,精简无用定义) + private static final int ANDROID_8_API = 26; // 悬浮窗类型适配(API26+必需) + private static final int ANDROID_10_API = 29; // API29+ 悬浮窗权限/参数适配 + private static final int ANDROID_19_API = 19; // 透明状态栏/导航栏适配 + + // 延迟初始化参数(让出主线程,避免启动阻塞) + private static final long DELAY_INIT_MS = 100L; + + // ====================== 成员属性区(按功能归类,命名规范) ====================== + // 延迟初始化核心 + private Handler mDelayHandler; // 延迟处理器(避免onCreate阻塞) + + // 通话监听核心 + private TelephonyManager mTelephonyManager; // 电话管理器(监听通话状态) + private PhoneStateListener mPhoneStateListener;// 通话状态监听回调 + private String mCallNumber; // 当前通话号码 + private boolean mIsCallingIn; // 是否为来电(true=来电,false=去电) + + // 悬浮窗核心 + private WindowManager mWindowManager; // 窗口管理器(添加/移除悬浮窗) + private WindowManager.LayoutParams mWindowParams;// 悬浮窗参数配置 + private View mPhoneCallView; // 通话悬浮窗根视图 + private TextView mTvCallNumber; // 悬浮窗号码显示控件 + private Button mBtnOpenApp; // 悬浮窗跳转APP按钮 + private boolean mHasShown; // 悬浮窗显示状态标记(避免重复操作) + + // ====================== Service生命周期方法区(按执行顺序排列) ====================== @Override public void onCreate() { super.onCreate(); - LogUtils.d(TAG, "onCreate: 服务启动,立即注册前台服务"); - // ========== 第一步:无条件优先执行前台服务注册,这是唯一必须同步做的事 ========== - boolean foregroundSuccess = startForegroundImmediately(); - if (!foregroundSuccess) { - LogUtils.e(TAG, "onCreate: 前台服务注册失败,直接停止服务"); - stopSelf(); - return; - } + LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 ====="); - // ========== 第二步:所有其他逻辑延迟 100ms 执行,让出主线程,避免阻塞 ========== - mDelayHandler = new Handler(Looper.getMainLooper()); - mDelayHandler.postDelayed(new Runnable() { - @Override - public void run() { - initDelayedLogic(); - } - }, 100); + // 延迟初始化所有逻辑(让出主线程,避免启动阻塞,提升启动速度) + initDelayHandlerAndLogic(); + + LogUtils.d(TAG, "===== onCreate: 通话监听服务启动完成 ====="); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId); + + // 加载服务配置,决定重启策略(启用则自动重启,禁用则默认) + MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class); + int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式")); + + return startMode; } @Override public IBinder onBind(Intent intent) { + LogUtils.d(TAG, "onBind: 服务无需绑定,返回null"); return null; } - /** - * 核心:仅负责前台服务注册,无任何其他逻辑,确保 5 秒内完成 - * @return true 注册成功,false 失败 - */ - private boolean startForegroundImmediately() { - try { - Notification notification = createForegroundNotification(); - if (Build.VERSION.SDK_INT >= ANDROID_10_API) { - startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_PHONE_CALL); - } else { - startForeground(NOTIFICATION_ID, notification); - } - LogUtils.d(TAG, "startForegroundImmediately: 前台服务注册成功"); - return true; - } catch (Exception e) { - LogUtils.e(TAG, "startForegroundImmediately: 注册失败", e); - return false; - } - } - - /** - * 简化通知创建逻辑,只保留必要参数,确保最快创建 - */ - private Notification createForegroundNotification() { - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - // 8.0+ 创建渠道,极简配置 - if (Build.VERSION.SDK_INT >= ANDROID_8_API && manager != null) { - NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, "通话监听", NotificationManager.IMPORTANCE_LOW); - channel.setSound(null, null); - channel.enableVibration(false); - manager.createNotificationChannel(channel); - } - - // 通知构建:必须确保小图标存在!!!替换为你项目中真实存在的图标资源 - Notification.Builder builder = new Notification.Builder(this); - // 重点:这里的 ic_launcher 必须是 res/drawable 下存在的图标,否则通知创建失败 - builder.setSmallIcon(R.drawable.ic_launcher); - builder.setContentTitle("通话监听"); - builder.setContentText("运行中"); - builder.setPriority(Notification.PRIORITY_LOW); - if (Build.VERSION.SDK_INT >= ANDROID_8_API) { - builder.setChannelId(CHANNEL_ID); - } - return builder.build(); - } - - /** - * 延迟初始化所有非核心逻辑:电话监听、悬浮窗 - */ - private void initDelayedLogic() { - LogUtils.d(TAG, "initDelayedLogic: 开始初始化延迟逻辑"); - initPhoneStateListener(); - initPhoneCallView(); - LogUtils.d(TAG, "initDelayedLogic: 延迟逻辑初始化完成"); - } - - private void initPhoneStateListener() { - phoneStateListener = new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String incomingNumber) { - super.onCallStateChanged(state, incomingNumber); - callNumber = incomingNumber; - switch (state) { - case TelephonyManager.CALL_STATE_IDLE: - dismiss(); - break; - case TelephonyManager.CALL_STATE_RINGING: - isCallingIn = true; - updateUI(); - show(); - break; - case TelephonyManager.CALL_STATE_OFFHOOK: - updateUI(); - show(); - break; - } - } - }; - - telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); - if (telephonyManager != null) { - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - } - } - - private void initPhoneCallView() { - windowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); - if (windowManager == null) { - return; - } - - int width = windowManager.getDefaultDisplay().getWidth(); - params = new WindowManager.LayoutParams(); - params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; - params.width = width; - params.height = WindowManager.LayoutParams.WRAP_CONTENT; - params.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; - params.format = PixelFormat.TRANSLUCENT; - - if (Build.VERSION.SDK_INT >= ANDROID_8_API) { - params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - } else { - params.type = WindowManager.LayoutParams.TYPE_PHONE; - } - - params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; - if (Build.VERSION.SDK_INT >= ANDROID_19_API) { - params.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; - } - - FrameLayout interceptorLayout = new FrameLayout(this) { - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - return true; - } - return super.dispatchKeyEvent(event); - } - }; - - phoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, interceptorLayout); - tvCallNumber = (TextView) phoneCallView.findViewById(R.id.tv_call_number); - btnOpenApp = (Button) phoneCallView.findViewById(R.id.btn_open_app); - btnOpenApp.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - PhoneCallService.CallType type = isCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT; - PhoneCallActivity.actionStart(CallListenerService.this, callNumber, type); - } - }); - } - - private void show() { - if (!hasShown && phoneCallView != null && windowManager != null) { - try { - windowManager.addView(phoneCallView, params); - hasShown = true; - } catch (Exception e) { - LogUtils.e(TAG, "show: 悬浮窗添加失败", e); - } - } - } - - private void dismiss() { - if (hasShown && phoneCallView != null && windowManager != null) { - try { - windowManager.removeView(phoneCallView); - } catch (Exception e) { - LogUtils.e(TAG, "dismiss: 悬浮窗移除失败", e); - } finally { - hasShown = false; - isCallingIn = false; - } - } - } - - private void updateUI() { - if (tvCallNumber == null) { - return; - } - String formatNumber = formatPhoneNumber(callNumber); - tvCallNumber.setText(formatNumber); - int drawableId = isCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out; - tvCallNumber.setCompoundDrawablesWithIntrinsicBounds(null, null, getResources().getDrawable(drawableId), null); - } - - public static String formatPhoneNumber(String phoneNum) { - if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) { - return phoneNum.substring(0, 3) + "-" + phoneNum.substring(3, 7) + "-" + phoneNum.substring(7); - } - return phoneNum; - } - @Override public void onDestroy() { super.onDestroy(); - dismiss(); - if (telephonyManager != null) { - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + LogUtils.d(TAG, "===== onDestroy: 通话监听服务开始销毁 ====="); + + // 全量清理资源,彻底避免内存泄漏 + dismissFloatWindow(); // 移除悬浮窗 + unregisterPhoneStateListener();// 注销通话监听 + clearDelayHandler(); // 清空延迟任务 + resetAllReferences(); // 置空所有成员属性 + + LogUtils.d(TAG, "===== onDestroy: 通话监听服务销毁完成 ====="); + } + + // ====================== 延迟初始化方法区(非阻塞启动,提升稳定性) ====================== + /** + * 初始化延迟处理器,执行核心逻辑(通话监听+悬浮窗) + */ + private void initDelayHandlerAndLogic() { + mDelayHandler = new Handler(Looper.getMainLooper()); + mDelayHandler.postDelayed(new Runnable() { + @Override + public void run() { + LogUtils.d(TAG, "initDelayHandlerAndLogic: 开始延迟初始化核心逻辑"); + initPhoneStateListener(); // 初始化通话状态监听 + initFloatWindow(); // 初始化通话悬浮窗 + LogUtils.d(TAG, "initDelayHandlerAndLogic: 延迟初始化完成,服务就绪"); + } + }, DELAY_INIT_MS); + } + + /** + * 初始化通话状态监听(注册TelephonyManager,响应通话状态变化) + */ + private void initPhoneStateListener() { + // 1. 创建通话状态监听回调 + mPhoneStateListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int callState, String incomingNumber) { + super.onCallStateChanged(callState, incomingNumber); + mCallNumber = incomingNumber; + LogUtils.d(TAG, "onCallStateChanged: 通话状态变化,状态=" + getCallStateDesc(callState) + ",号码=" + incomingNumber); + + // 响应不同通话状态 + switch (callState) { + case TelephonyManager.CALL_STATE_IDLE: + // 通话空闲(挂断/未通话):隐藏悬浮窗 + dismissFloatWindow(); + break; + case TelephonyManager.CALL_STATE_RINGING: + // 来电响铃:标记来电状态,更新UI并显示悬浮窗 + mIsCallingIn = true; + updateFloatWindowUI(); + showFloatWindow(); + break; + case TelephonyManager.CALL_STATE_OFFHOOK: + // 通话中(接听/拨号):更新UI并显示悬浮窗 + updateFloatWindowUI(); + showFloatWindow(); + break; + } + } + }; + + // 2. 注册通话监听(非空校验,避免崩溃) + mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); + if (mTelephonyManager != null) { + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + LogUtils.d(TAG, "initPhoneStateListener: 通话状态监听注册成功"); + } else { + LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager获取失败,监听注册失败"); } + } + + /** + * 初始化通话悬浮窗(配置参数+加载布局+绑定事件,适配API29-30) + */ + private void initFloatWindow() { + // 1. 获取窗口管理器(非空校验,避免后续崩溃) + mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + if (mWindowManager == null) { + LogUtils.e(TAG, "initFloatWindow: WindowManager获取失败,悬浮窗初始化失败"); + return; + } + + // 2. 配置悬浮窗参数(精准适配API29+,兼容悬浮窗权限) + initFloatWindowParams(); + + // 3. 加载悬浮窗布局(添加返回键拦截,避免误关闭) + FrameLayout keyInterceptorLayout = new FrameLayout(this) { + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // 拦截返回键,保障通话时悬浮窗正常显示 + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + LogUtils.d(TAG, "dispatchKeyEvent: 拦截悬浮窗返回键事件"); + return true; + } + return super.dispatchKeyEvent(event); + } + }; + mPhoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, keyInterceptorLayout); + + // 4. 绑定悬浮窗控件,设置跳转按钮事件 + bindFloatWindowViews(); + + LogUtils.d(TAG, "initFloatWindow: 悬浮窗初始化完成"); + } + + /** + * 配置悬浮窗参数(适配API29+窗口类型,确保正常显示) + */ + private void initFloatWindowParams() { + mWindowParams = new WindowManager.LayoutParams(); + // 窗口位置:顶部居中 + mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + // 窗口大小:宽度全屏,高度自适应 + mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT; + mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + // 固定竖屏显示 + mWindowParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; + // 窗口格式:半透明 + mWindowParams.format = PixelFormat.TRANSLUCENT; + + // 窗口类型(API29+ 强制用 TYPE_APPLICATION_OVERLAY,需悬浮窗权限) + if (Build.VERSION.SDK_INT >= ANDROID_10_API) { + mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + LogUtils.d(TAG, "initFloatWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY(需开启悬浮窗权限)"); + } else if (Build.VERSION.SDK_INT >= ANDROID_8_API) { + mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE; + } + + // 窗口标志:无焦点(不抢占输入)、全屏布局、兼容透明状态栏/导航栏 + mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + if (Build.VERSION.SDK_INT >= ANDROID_19_API) { + mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + } + } + + /** + * 绑定悬浮窗控件,设置跳转通话详情页事件 + */ + private void bindFloatWindowViews() { + mTvCallNumber = (TextView) mPhoneCallView.findViewById(R.id.tv_call_number); + mBtnOpenApp = (Button) mPhoneCallView.findViewById(R.id.btn_open_app); + + // 跳转按钮点击事件 + mBtnOpenApp.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (TextUtils.isEmpty(mCallNumber)) { + LogUtils.w(TAG, "bindFloatWindowViews: 通话号码为空,跳过跳转"); + return; + } + LogUtils.d(TAG, "bindFloatWindowViews: 点击跳转通话详情页,号码=" + mCallNumber); + PhoneCallService.CallType callType = mIsCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT; + PhoneCallActivity.actionStart(CallListenerService.this, mCallNumber, callType); + } + }); + } + + // ====================== 悬浮窗功能逻辑区(显示/隐藏/更新UI) ====================== + /** + * 显示通话悬浮窗(避免重复添加,防止窗口泄露) + */ + private void showFloatWindow() { + if (!mHasShown && mPhoneCallView != null && mWindowManager != null) { + try { + mWindowManager.addView(mPhoneCallView, mWindowParams); + mHasShown = true; + LogUtils.d(TAG, "showFloatWindow: 悬浮窗显示成功"); + } catch (SecurityException e) { + LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示失败(无悬浮窗权限,需引导用户开启)", e); + } catch (Exception e) { + LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示异常", e); + } + } else { + LogUtils.d(TAG, "showFloatWindow: 悬浮窗已显示/组件未初始化,跳过显示"); + } + } + + /** + * 隐藏通话悬浮窗(避免重复移除,防止崩溃) + */ + private void dismissFloatWindow() { + if (mHasShown && mPhoneCallView != null && mWindowManager != null) { + try { + mWindowManager.removeView(mPhoneCallView); + LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗隐藏成功"); + } catch (Exception e) { + LogUtils.e(TAG, "dismissFloatWindow: 悬浮窗隐藏异常", e); + } finally { + mHasShown = false; + mIsCallingIn = false; // 重置来电状态标记 + } + } else { + LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗已隐藏/组件未初始化,跳过隐藏"); + } + } + + /** + * 更新悬浮窗UI(显示格式化号码+通话类型图标) + */ + private void updateFloatWindowUI() { + if (mTvCallNumber == null || TextUtils.isEmpty(mCallNumber)) { + LogUtils.w(TAG, "updateFloatWindowUI: 控件未初始化/号码为空,更新失败"); + return; + } + + // 格式化11位手机号(3-4-4分隔,提升可读性) + String formattedNumber = formatPhoneNumber(mCallNumber); + mTvCallNumber.setText(formattedNumber); + + // 设置通话类型图标(来电/去电区分) + int iconResId = mIsCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out; + mTvCallNumber.setCompoundDrawablesWithIntrinsicBounds( + null, null, getResources().getDrawable(iconResId), null + ); + LogUtils.d(TAG, "updateFloatWindowUI: 悬浮窗UI更新完成,号码=" + formattedNumber + ",类型=" + (mIsCallingIn ? "来电" : "去电")); + } + + // ====================== 资源清理方法区(服务销毁时全量释放) ====================== + /** + * 注销通话状态监听(释放TelephonyManager资源) + */ + private void unregisterPhoneStateListener() { + if (mTelephonyManager != null && mPhoneStateListener != null) { + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); + LogUtils.d(TAG, "unregisterPhoneStateListener: 通话监听已注销"); + } + mTelephonyManager = null; + mPhoneStateListener = null; + } + + /** + * 清空延迟处理器(移除未执行任务,避免内存泄漏) + */ + private void clearDelayHandler() { if (mDelayHandler != null) { mDelayHandler.removeCallbacksAndMessages(null); + mDelayHandler = null; + LogUtils.d(TAG, "clearDelayHandler: 延迟处理器已清空"); + } + } + + /** + * 置空所有成员属性(彻底释放引用,避免内存泄漏) + */ + private void resetAllReferences() { + mCallNumber = null; + mPhoneCallView = null; + mWindowParams = null; + mWindowManager = null; + mTvCallNumber = null; + mBtnOpenApp = null; + } + + // ====================== 工具方法区(通用辅助功能,独立归类) ====================== + /** + * 格式化手机号(11位手机号:3-4-4分隔,非11位保持原格式) + * @param phoneNum 待格式化的手机号 + * @return 格式化后的号码 + */ + public static String formatPhoneNumber(String phoneNum) { + if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) { + String formatted = phoneNum.substring(0, 3) + "-" + + phoneNum.substring(3, 7) + "-" + + phoneNum.substring(7); + LogUtils.d(TAG, "formatPhoneNumber: 号码格式化,原=" + phoneNum + ",新=" + formatted); + return formatted; + } + LogUtils.d(TAG, "formatPhoneNumber: 非11位号码,无需格式化,号码=" + phoneNum); + return phoneNum; + } + + /** + * 转换通话状态为文字描述(便于日志查看,快速定位问题) + * @param callState 通话状态(TelephonyManager常量) + * @return 状态描述文字 + */ + private String getCallStateDesc(int callState) { + switch (callState) { + case TelephonyManager.CALL_STATE_IDLE: + return "空闲(挂断/未通话)"; + case TelephonyManager.CALL_STATE_RINGING: + return "响铃(来电)"; + case TelephonyManager.CALL_STATE_OFFHOOK: + return "通话中(接听/拨号)"; + default: + return "未知状态"; } - // 清空所有引用 - phoneStateListener = null; - telephonyManager = null; - windowManager = null; - phoneCallView = null; } } 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 001a427..22930c8 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 @@ -252,12 +252,8 @@ public class AssistantService extends Service { Intent intent = new Intent(this, MainService.class); // 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService) - if (Build.VERSION.SDK_INT >= ANDROID_12_API) { - LogUtils.d(TAG, "wakeupAndBindMain: 应用后台,启动主服务为前台服务"); - startForegroundService(intent); - } else { - startService(intent); - } + startForegroundService(intent); + // BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开 bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT); LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService"); 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 c465cff..db08f8e 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 @@ -32,68 +32,76 @@ import java.util.TimerTask; * @Date 2025/02/13 06:56:41 * @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动 * 严格适配 Android API 30 + Java 7 语法规范 | 解决前台服务启动超时崩溃 - * 优化:移除延迟启动,非核心服务直接在 mainService() 中启动 + * 核心优化:1. 移除延迟启动逻辑 2. 标准化日志管理 3. 强化资源清理 4. 结构分层重构 */ public class MainService extends Service { - // ====================== 常量定义区(全硬编码,杜绝高版本 API 依赖) ====================== + + // ====================== 常量定义区(全硬编码,无高版本API依赖) ====================== public static final String TAG = "MainService"; public static final int MSG_UPDATE_STATUS = 0; - // 铃声音量检查定时器参数 - private static final long VOLUME_CHECK_DELAY = 1000L; - private static final long VOLUME_CHECK_PERIOD = 60000L; - // 前台服务通知配置 + + // 铃声音量监控参数(定时检查+恢复) + private static final long VOLUME_CHECK_DELAY = 1000L; // 首次检查延迟1s + private static final long VOLUME_CHECK_PERIOD = 60000L; // 后续每60s检查一次 + + // 前台服务配置(固定ID+渠道,避免重复创建) private static final String FOREGROUND_CHANNEL_ID = "main_service_foreground_channel"; private static final int FOREGROUND_NOTIFICATION_ID = 1001; - // 前台服务类型硬编码:dataSync(0x01) | 替代 Build.VERSION_CODES 高版本常量 - private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001; - // Android 版本常量硬编码 | Java 7 不支持 Build.VERSION_CODES 高版本引用 - private static final int ANDROID_8_API = 26; - private static final int ANDROID_10_API = 29; - private static final int ANDROID_12_API = 31; - // 重试延迟时间(仅保留守护服务重绑定延迟,移除非核心服务延迟) + private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001; // dataSync类型硬编码 + + // Android版本常量(替代Build.VERSION_CODES,适配Java7) + private static final int ANDROID_8_API = 26; // Android 8.0 + private static final int ANDROID_10_API = 29; // Android 10 + private static final int ANDROID_12_API = 31; // Android 12 + + // 守护服务重绑定延迟(仅保留核心重试逻辑) private static final long RETRY_DELAY_MS = 3000L; - // ====================== 静态成员变量区 ====================== - private static MainService sMainServiceInstance; - private static volatile TomCat sTomCatInstance; + // ====================== 静态成员属性区(全局共享实例,统一前缀s) ====================== + private static MainService sMainServiceInstance; // 主服务全局实例 + private static volatile TomCat sTomCatInstance; // 号码识别核心实例(volatile保证可见性) - // ====================== 成员变量区 ====================== - 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; + // ====================== 成员属性区(业务+UI+资源,统一前缀m) ====================== + private volatile boolean mIsServiceRunning; // 服务运行状态标记(volatile防指令重排) + private MainServiceBean mMainServiceBean; // 服务配置实体(启用状态存储) + private MainServiceHandler mMainServiceHandler; // 服务消息处理器(主线程通信) + private MyServiceConnection mServiceConnection; // 守护服务连接实例 + private AssistantService mAssistantService; // 绑定的守护服务实例 + private boolean mIsAssistantBound; // 守护服务绑定状态标记 + private MainReceiver mMainReceiver; // 全局广播接收器(监听系统事件) + private Timer mVolumeCheckTimer; // 铃声音量检查定时器(定时恢复配置) - // ====================== Binder 内部类 ====================== + // ====================== 内部类:Binder(服务绑定通信,优先定义) ====================== public class MyBinder extends Binder { + /** + * 外部组件绑定服务时,获取主服务实例 + * @return MainService 主服务实例 + */ public MainService getService() { - LogUtils.d(TAG, "MyBinder.getService: 获取 MainService 实例"); + LogUtils.d(TAG, "MyBinder.getService: 外部获取主服务实例"); return MainService.this; } } - // ====================== ServiceConnection 内部类 ====================== + // ====================== 内部类: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; + LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的IBinder为空,绑定失败"); + mIsAssistantBound = false; return; } try { + // 类型转换获取守护服务实例 AssistantService.MyBinder binder = (AssistantService.MyBinder) service; mAssistantService = binder.getService(); - mIsBound = true; - LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 守护服务绑定成功 | AssistantService=" + mAssistantService); + mIsAssistantBound = true; + LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 守护服务绑定成功"); } catch (ClassCastException e) { - LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e); - mIsBound = false; + LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder类型转换失败", e); + mIsAssistantBound = false; } } @@ -101,134 +109,166 @@ public class MainService extends Service { public void onServiceDisconnected(ComponentName name) { LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 守护服务连接断开"); mAssistantService = null; - mIsBound = false; + mIsAssistantBound = false; - // 服务启用则重试绑定 + // 服务启用状态下,重试绑定守护服务(主服务存活核心保障) if (mMainServiceBean != null && mMainServiceBean.isEnable()) { - LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 重新唤醒并绑定守护服务"); + LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: " + RETRY_DELAY_MS + "ms后重试绑定守护服务"); new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run() { - wakeupAndBindAssistant(); + wakeupAndBindAssistantService(); } }, RETRY_DELAY_MS); } else { - LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 服务未启用,跳过重试"); + LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务已禁用,跳过重试绑定"); } } } - // ====================== 对外静态方法区 ====================== + // ====================== 对外静态方法区(服务启停/重启/状态查询,全局调用) ====================== + /** + * 检查号码是否在BoBullToon库中(外部组件调用,静态入口) + * @param phone 待查询号码 + * @return true=是BoBullToon号码,false=否/初始化失败 + */ public static boolean isPhoneInBoBullToon(String phone) { if (sTomCatInstance != null && phone != null && !phone.isEmpty()) { - return sTomCatInstance.isPhoneBoBullToon(phone); + boolean result = sTomCatInstance.isPhoneBoBullToon(phone); + LogUtils.d(TAG, "isPhoneInBoBullToon: 号码" + phone + "查询结果:" + (result ? "是" : "否")); + return result; } - LogUtils.w(TAG, "isPhoneInBoBullToon: TomCat 未初始化或号码为空"); + LogUtils.w(TAG, "isPhoneInBoBullToon: TomCat未初始化或号码为空,查询失败"); return false; } + /** + * 停止主服务(仅停止,不修改配置) + * @param context 上下文(非空校验) + */ public static void stopMainService(Context context) { if (context == null) { - LogUtils.e(TAG, "stopMainService: Context 为 null,无法执行"); + LogUtils.e(TAG, "stopMainService: 上下文为空,无法停止服务"); return; } LogUtils.d(TAG, "stopMainService: 执行停止主服务操作"); context.stopService(new Intent(context, MainService.class)); } + /** + * 启动主服务(仅启动,不修改配置) + * @param context 上下文(非空校验) + */ public static void startMainService(Context context) { if (context == null) { - LogUtils.e(TAG, "startMainService: Context 为 null,无法执行"); + LogUtils.e(TAG, "startMainService: 上下文为空,无法启动服务"); return; } - LogUtils.d(TAG, "startMainService: 执行启动主服务操作"); + LogUtils.d(TAG, "startMainService: 执行启动主服务操作(前台服务模式)"); Intent intent = new Intent(context, MainService.class); - context.startForegroundService(intent); + context.startForegroundService(intent); } + /** + * 重启主服务(先停后启,需服务已启用) + * @param context 上下文(非空校验) + */ public static void restartMainService(Context context) { if (context == null) { - LogUtils.e(TAG, "restartMainService: Context 为 null,无法执行"); + LogUtils.e(TAG, "restartMainService: 上下文为空,无法重启服务"); return; } - LogUtils.d(TAG, "restartMainService: 执行重启主服务操作"); + LogUtils.d(TAG, "restartMainService: 执行主服务重启流程"); - MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class); - if (bean != null && bean.isEnable()) { + MainServiceBean config = MainServiceBean.loadBean(context, MainServiceBean.class); + if (config != null && config.isEnable()) { stopMainService(context); startMainService(context); LogUtils.i(TAG, "restartMainService: 主服务重启完成"); } else { - LogUtils.w(TAG, "restartMainService: 主服务配置未启用,跳过重启"); + LogUtils.w(TAG, "restartMainService: 服务未启用或配置为空,跳过重启"); } } + /** + * 停止服务并保存禁用状态(更新配置+停止服务) + * @param context 上下文(非空校验) + */ public static void stopMainServiceAndSaveStatus(Context context) { if (context == null) { - LogUtils.e(TAG, "stopMainServiceAndSaveStatus: Context 为 null,无法执行"); + LogUtils.e(TAG, "stopMainServiceAndSaveStatus: 上下文为空,操作失败"); return; } - LogUtils.d(TAG, "stopMainServiceAndSaveStatus: 停止服务并保存禁用状态"); - MainServiceBean bean = new MainServiceBean(); - bean.setIsEnable(false); - MainServiceBean.saveBean(context, bean); + LogUtils.d(TAG, "stopMainServiceAndSaveStatus: 保存禁用状态并停止服务"); + MainServiceBean config = new MainServiceBean(); + config.setIsEnable(false); + MainServiceBean.saveBean(context, config); stopMainService(context); } + /** + * 启动服务并保存启用状态(更新配置+启动服务,先停后启避免重复) + * @param context 上下文(非空校验) + */ public static void startMainServiceAndSaveStatus(Context context) { if (context == null) { - LogUtils.e(TAG, "startMainServiceAndSaveStatus: Context 为 null,无法执行"); + LogUtils.e(TAG, "startMainServiceAndSaveStatus: 上下文为空,操作失败"); return; } - LogUtils.d(TAG, "startMainServiceAndSaveStatus: 启动服务并保存启用状态"); - MainServiceBean bean = new MainServiceBean(); - bean.setIsEnable(true); - MainServiceBean.saveBean(context, bean); - stopMainService(context); + LogUtils.d(TAG, "startMainServiceAndSaveStatus: 保存启用状态并启动服务"); + MainServiceBean config = new MainServiceBean(); + config.setIsEnable(true); + MainServiceBean.saveBean(context, config); + stopMainService(context); // 先停止旧服务,避免冲突 startMainService(context); } - // ====================== 成员方法区 ====================== + // ====================== 核心工具方法区(服务状态检查+前台通知创建,通用功能) ====================== /** - * 补充缺失的 appenMessage 方法 + * 补充消息追加方法(外部组件向服务发送消息) + * @param message 待追加消息(空值防护) */ public void appenMessage(String message) { String msg = message == null ? "null" : message; - LogUtils.d(TAG, "追加消息: " + msg); + LogUtils.d(TAG, "appenMessage: 接收外部消息:" + msg); if (mMainServiceHandler != null) { android.os.Message handlerMsg = android.os.Message.obtain(); handlerMsg.what = MSG_UPDATE_STATUS; handlerMsg.obj = msg; mMainServiceHandler.sendMessage(handlerMsg); + LogUtils.d(TAG, "appenMessage: 消息已发送至Handler处理"); + } else { + LogUtils.w(TAG, "appenMessage: MainServiceHandler未初始化,消息发送失败"); } } /** - * 创建前台服务通知 + * 创建前台服务通知(Android8.0+需渠道,低版本兼容) + * @return Notification 前台服务通知实例 */ private Notification createForegroundNotification() { - // 1. 创建通知渠道(Android 8.0+ 必需) + // 1. Android8.0+创建通知渠道(必需,否则通知不显示) if (Build.VERSION.SDK_INT >= ANDROID_8_API) { NotificationChannel channel = new NotificationChannel( - FOREGROUND_CHANNEL_ID, - "拨号主服务", - NotificationManager.IMPORTANCE_LOW + FOREGROUND_CHANNEL_ID, + "拨号主服务", + NotificationManager.IMPORTANCE_LOW ); - channel.setDescription("主服务后台运行,保障拨号功能正常"); - channel.setSound(null, null); - channel.enableVibration(false); + channel.setDescription("主服务后台运行,保障通话监听与号码识别功能正常"); + channel.setSound(null, null); // 关闭通知声音 + channel.enableVibration(false); // 关闭振动 - NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - if (manager != null) { - manager.createNotificationChannel(channel); - LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功"); + NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + LogUtils.d(TAG, "createForegroundNotification: Android8.0+通知渠道创建成功"); } else { - LogUtils.e(TAG, "createForegroundNotification: NotificationManager 获取失败"); + LogUtils.e(TAG, "createForegroundNotification: NotificationManager获取失败,渠道创建失败"); } } - // 2. 构建通知 + // 2. 构建通知实例(分版本兼容Builder) Notification.Builder builder; if (Build.VERSION.SDK_INT >= ANDROID_8_API) { builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID); @@ -237,37 +277,43 @@ public class MainService extends Service { } builder.setSmallIcon(R.drawable.ic_launcher); builder.setContentTitle("拨号服务运行中"); - builder.setContentText("正在后台保障通话监听与号码识别"); - builder.setPriority(Notification.PRIORITY_LOW); - builder.setOngoing(true); + builder.setContentText("后台保障通话监听与号码识别,请勿手动关闭"); + builder.setPriority(Notification.PRIORITY_LOW); // 低优先级,不打扰用户 + builder.setOngoing(true); // 不可手动清除,保障服务存活 + LogUtils.d(TAG, "createForegroundNotification: 前台服务通知构建完成"); return builder.build(); } /** - * 检查服务是否正在运行 + * 检查指定服务是否正在运行(通过ActivityManager查询) + * @param serviceClass 待检查服务类 + * @return true=运行中,false=未运行/查询失败 */ private boolean isServiceRunning(Class serviceClass) { if (serviceClass == null) { - LogUtils.e(TAG, "isServiceRunning: 服务类参数为 null"); + LogUtils.e(TAG, "isServiceRunning: 服务类为空,检查失败"); return false; } - ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - if (manager == null) { - LogUtils.w(TAG, "isServiceRunning: ActivityManager 获取失败"); + ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + if (activityManager == null) { + LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败,检查失败"); return false; } - for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) { - if (serviceClass.getName().equals(info.service.getClassName())) { + // 遍历运行中服务,匹配类名 + for (ActivityManager.RunningServiceInfo serviceInfo : activityManager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(serviceInfo.service.getClassName())) { + LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "正在运行"); return true; } } + LogUtils.d(TAG, "isServiceRunning: 服务" + serviceClass.getSimpleName() + "未运行"); return false; } - // ====================== Service 生命周期方法区 ====================== + // ====================== Service生命周期方法区(按执行顺序:创建→绑定→启动→销毁) ====================== @Override public void onCreate() { super.onCreate(); @@ -275,74 +321,72 @@ public class MainService extends Service { sMainServiceInstance = this; mIsServiceRunning = false; - // 仅初始化核心组件(移除 mDelayHandler 初始化,无需延迟启动) + // 初始化核心组件(无延迟,直接初始化) mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); - mMyServiceConnection = new MyServiceConnection(); + mServiceConnection = new MyServiceConnection(); mMainServiceHandler = new MainServiceHandler(this); - // 初始化铃声音量检查定时器 + // 初始化音量监控定时器(服务启动即开启,保障音量配置) initVolumeCheckTimer(); - // 启动服务 - mainService(); + // 执行核心业务启动逻辑(无延迟,优先启动) + startCoreBusiness(); - LogUtils.d(TAG, "===== onCreate: 主服务创建完成(仅初始化组件) ====="); + LogUtils.d(TAG, "===== onCreate: 主服务创建完成 ====="); } @Override public IBinder onBind(Intent intent) { - LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent); + LogUtils.d(TAG, "onBind: 主服务被外部组件绑定,Intent=" + (intent != null ? intent.getAction() : "null")); return new MyBinder(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { - LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId); - mainService(); - return (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + LogUtils.d(TAG, "onStartCommand: 主服务被启动,startId=" + startId); + // 重复启动时再次执行核心业务(避免服务被杀死后重启失败) + startCoreBusiness(); + + // 服务启用则返回START_STICKY(系统杀死后自动重启),否则默认行为 + int result = (mMainServiceBean != null && mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (result == START_STICKY ? "START_STICKY(自动重启)" : "默认模式")); + return result; } - // ====================== onDestroy:强制全量清理,不改动 Bean 配置 ====================== @Override public void onDestroy() { super.onDestroy(); - LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁(强制清理所有资源) ====="); + LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁,全量清理资源 ====="); - // 2. 释放定时器资源 + // 1. 停止音量监控定时器(释放线程资源) cancelVolumeCheckTimer(); - // 3. 强制解除守护服务绑定 - if (mIsBound && mMyServiceConnection != null) { + // 2. 解除守护服务绑定(避免内存泄漏) + if (mIsAssistantBound && mServiceConnection != null) { try { - unbindService(mMyServiceConnection); - LogUtils.d(TAG, "onDestroy: 解除守护服务绑定成功"); + unbindService(mServiceConnection); + LogUtils.d(TAG, "onDestroy: 守护服务绑定已解除"); } catch (IllegalArgumentException e) { - LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e); + LogUtils.w(TAG, "onDestroy: 解除守护服务绑定失败(服务未绑定)", e); } - mIsBound = false; - mMyServiceConnection = null; + mIsAssistantBound = false; + mServiceConnection = null; } - // 4. 强制注销广播接收器 + // 3. 注销广播接收器(释放系统资源) if (mMainReceiver != null) { mMainReceiver.unregisterAction(this); mMainReceiver = null; - LogUtils.d(TAG, "onDestroy: 广播接收器已注销"); + LogUtils.d(TAG, "onDestroy: 全局广播接收器已注销"); } - // 5. 强制停止主业务线程 -// if (mMainServiceThread != null) { -// mMainServiceThread.setIsExit(true); -// mMainServiceThread = null; -// } - - // 6. 强制停止所有子服务 + // 4. 停止所有子服务(强制停止,避免子服务残留) stopService(new Intent(this, AssistantService.class)); stopService(new Intent(this, CallListenerService.class)); stopService(new Intent(this, MyCallScreeningService.class)); LogUtils.d(TAG, "onDestroy: 所有子服务已强制停止"); - // 7. 清空静态实例,避免内存泄漏(不改动 Bean 配置) + // 5. 清空所有引用(静态+成员,彻底避免内存泄漏,不修改配置Bean) sMainServiceInstance = null; sTomCatInstance = null; mMainServiceHandler = null; @@ -351,155 +395,201 @@ public class MainService extends Service { // 标记服务为未运行 mIsServiceRunning = false; - LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成(资源全清理,Bean 配置无改动) ====="); + LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成,资源全清理 ====="); } - // ====================== 核心业务逻辑:非核心服务直接启动,无延迟 ====================== - private synchronized void mainService() { - LogUtils.d(TAG, "mainService: 执行核心业务逻辑"); - - // 第一步:优先判断服务是否已运行,已运行则直接退出(不做任何动作) - if (!mIsServiceRunning) { - mIsServiceRunning = true; - // 第二步:加载最新配置,校验服务是否启用(未启用则退出) - mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); - if (mMainServiceBean == null && mMainServiceBean.isEnable()) { - // 第三步:配置启用且服务未运行,执行启动流程 - LogUtils.i(TAG, "mainService: 服务未运行且配置已启用,开始启动核心流程"); - - // 1. 启动前台服务(优先执行,避免超时) - try { - Notification foregroundNotification = createForegroundNotification(); - startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC); - //startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification); - } catch (IllegalArgumentException e) { - LogUtils.e(TAG, "mainService: 启动前台服务失败,类型不匹配", e); - mIsServiceRunning = false; - stopSelf(); - return; - } catch (Exception e) { - LogUtils.e(TAG, "mainService: 启动前台服务异常", e); - mIsServiceRunning = false; - stopSelf(); - return; - } - - // 2. 绑定守护服务 - wakeupAndBindAssistant(); - - // 3. 初始化业务组件 - initTomCat(); - initMainReceiver(); - Rules.getInstance(this).loadRules(); - LogUtils.d(TAG, "mainService: 黑白名单规则已加载"); - - // 4. 直接启动电话监听服务(移除延迟,校验通过则立即启动) - Intent callListenerIntent = new Intent(this, CallListenerService.class); - try { - startForegroundService(callListenerIntent); - //startService(callListenerIntent); - } catch (Exception e) { - LogUtils.e(TAG, "mainService: CallListenerService 启动失败", e); - } - - - // 5. 直接启动通话筛选服务(API 10+ 生效,无延迟) - Intent screeningIntent = new Intent(this, MyCallScreeningService.class); - try { - startForegroundService(screeningIntent); - //startService(screeningIntent); - } catch (Exception e) { - LogUtils.e(TAG, "mainService: MyCallScreeningService 启动失败", e); - } - - // 6. 启动主业务线程 -// mMainServiceThread = MainServiceThread.getInstance(this, mMainServiceHandler); -// mMainServiceThread.start(); - LogUtils.i(TAG, "mainService: 核心流程启动完成,主服务正常运行"); - } - } - } - - private void wakeupAndBindAssistant() { - if (mMyServiceConnection == null) { - LogUtils.e(TAG, "wakeupAndBindAssistant: 初始化失败,绑定失败"); + // ====================== 核心业务逻辑区(服务启动核心流程,无延迟) ====================== + /** + * 启动核心业务(主服务核心入口,避免重复启动) + */ + private synchronized void startCoreBusiness() { + // 服务已运行则直接返回,避免重复执行启动流程 + if (mIsServiceRunning) { + LogUtils.d(TAG, "startCoreBusiness: 主服务已运行,跳过重复启动"); return; } - Intent intent = new Intent(this, AssistantService.class); - if (Build.VERSION.SDK_INT >= ANDROID_10_API) { - LogUtils.d(TAG, "wakeupAndBindAssistant: 应用后台,启动 AssistantService 为前台服务"); - startForegroundService(intent); - } else { - startService(intent); + mIsServiceRunning = true; + + // 重新加载最新配置(避免配置修改后未生效) + mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class); + if (mMainServiceBean == null || !mMainServiceBean.isEnable()) { + LogUtils.w(TAG, "startCoreBusiness: 服务配置未启用或配置为空,启动流程终止"); + mIsServiceRunning = false; + stopSelf(); // 未启用则主动停止服务 + return; } - bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT); - LogUtils.d(TAG, "wakeupAndBindAssistant: 已启动并绑定守护服务"); + + LogUtils.i(TAG, "startCoreBusiness: 服务配置已启用,启动核心流程"); + + // 1. 优先启动前台服务(避免前台服务启动超时崩溃,核心优先级) + try { + Notification foregroundNotification = createForegroundNotification(); + startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC); + LogUtils.i(TAG, "startCoreBusiness: 前台服务启动成功,通知ID=" + FOREGROUND_NOTIFICATION_ID); + } catch (IllegalArgumentException e) { + LogUtils.e(TAG, "startCoreBusiness: 前台服务启动失败(服务类型不匹配)", e); + mIsServiceRunning = false; + stopSelf(); + return; + } catch (Exception e) { + LogUtils.e(TAG, "startCoreBusiness: 前台服务启动异常", e); + mIsServiceRunning = false; + stopSelf(); + return; + } + + // 2. 绑定守护服务(保障主服务存活,防系统杀死) + wakeupAndBindAssistantService(); + + // 3. 初始化核心业务组件(号码识别+黑白名单规则) + initTomCatComponent(); + initRulesConfig(); + + // 4. 启动通话监听相关服务(无延迟,直接启动,保障功能生效) + startCallRelatedServices(); + + LogUtils.i(TAG, "startCoreBusiness: 主服务核心流程启动完成,服务正常运行"); } - // ====================== 铃声音量监控相关方法区 ====================== + /** + * 唤醒并绑定守护服务(分版本适配启动方式,Android10+前台服务启动) + */ + private void wakeupAndBindAssistantService() { + if (mServiceConnection == null) { + LogUtils.e(TAG, "wakeupAndBindAssistantService: 服务连接实例未初始化,绑定失败"); + return; + } + + Intent assistantIntent = new Intent(this, AssistantService.class); + // Android10+应用后台启动服务需用前台服务模式 + LogUtils.d(TAG, "wakeupAndBindAssistantService: Android10+,前台服务模式启动守护服务"); + startForegroundService(assistantIntent); + + // 绑定守护服务(BIND_IMPORTANT:高优先级绑定,断开时回调) + bindService(assistantIntent, mServiceConnection, Context.BIND_IMPORTANT); + LogUtils.d(TAG, "wakeupAndBindAssistantService: 守护服务启动并发起绑定请求"); + } + + // ====================== 铃声音量监控方法区(定时检查+恢复配置) ====================== + /** + * 初始化音量检查定时器(先取消旧定时器,避免重复创建) + */ private void initVolumeCheckTimer() { cancelVolumeCheckTimer(); - mStreamVolumeCheckTimer = new Timer(); - mStreamVolumeCheckTimer.schedule(new TimerTask() { + mVolumeCheckTimer = new Timer(); + mVolumeCheckTimer.schedule(new TimerTask() { @Override public void run() { checkAndRestoreRingerVolume(); } }, VOLUME_CHECK_DELAY, VOLUME_CHECK_PERIOD); - LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量检查定时器已启动"); + LogUtils.d(TAG, "initVolumeCheckTimer: 铃声音量监控定时器启动,周期" + VOLUME_CHECK_PERIOD + "ms"); } + /** + * 检查并恢复铃声音量(对比配置值,不一致则恢复,保障用户配置生效) + */ private void checkAndRestoreRingerVolume() { AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); if (audioManager == null) { - LogUtils.e(TAG, "checkAndRestoreRingerVolume: 获取 AudioManager 失败"); + 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: 铃音配置未存在,已初始化默认配置"); + // 加载音量配置(无配置则初始化默认值) + RingTongBean ringConfig = RingTongBean.loadBean(this, RingTongBean.class); + if (ringConfig == null) { + ringConfig = new RingTongBean(); + RingTongBean.saveBean(this, ringConfig); + LogUtils.d(TAG, "checkAndRestoreRingerVolume: 音量配置未存在,初始化默认配置"); + return; } - // 检查并恢复音量 try { int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING); - int configVolume = ringTongBean.getStreamVolume(); + int configVolume = ringConfig.getStreamVolume(); + // 音量不一致则恢复配置值 if (currentVolume != configVolume) { audioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0); - LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃声音量已恢复 | 配置值=" + configVolume + " | 当前值=" + currentVolume); + LogUtils.d(TAG, "checkAndRestoreRingerVolume: 铃声音量已恢复,配置值=" + configVolume + ",原当前值=" + currentVolume); + } else { + LogUtils.v(TAG, "checkAndRestoreRingerVolume: 铃声音量与配置一致,无需调整"); } } catch (SecurityException e) { - LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足", e); + LogUtils.e(TAG, "checkAndRestoreRingerVolume: 音量设置权限不足,恢复失败", e); } } + /** + * 取消音量检查定时器(释放Timer资源,避免内存泄漏) + */ private void cancelVolumeCheckTimer() { - if (mStreamVolumeCheckTimer != null) { - mStreamVolumeCheckTimer.cancel(); - mStreamVolumeCheckTimer = null; - LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量检查定时器已取消"); + if (mVolumeCheckTimer != null) { + mVolumeCheckTimer.cancel(); + mVolumeCheckTimer = null; + LogUtils.d(TAG, "cancelVolumeCheckTimer: 铃声音量监控定时器已取消"); } } - // ====================== 辅助初始化方法区 ====================== - private void initTomCat() { + // ====================== 辅助初始化方法区(业务组件初始化,统一归类) ====================== + /** + * 初始化TomCat组件(号码识别核心,加载本地数据) + */ + private void initTomCatComponent() { sTomCatInstance = TomCat.getInstance(this); - if (!sTomCatInstance.loadPhoneBoBullToon()) { - LogUtils.w(TAG, "initTomCat: 未下载 BoBullToon 数据,参数加载失败"); + if (sTomCatInstance.loadPhoneBoBullToon()) { + LogUtils.d(TAG, "initTomCatComponent: BoBullToon号码库加载成功"); } else { - LogUtils.d(TAG, "initTomCat: BoBullToon 数据加载成功"); + LogUtils.w(TAG, "initTomCatComponent: BoBullToon号码库未下载,加载失败(不影响服务运行)"); } } + /** + * 初始化黑白名单规则配置(加载本地规则,保障通话筛选生效) + */ + private void initRulesConfig() { + Rules rules = Rules.getInstance(this); + if (rules != null) { + rules.loadRules(); + LogUtils.d(TAG, "initRulesConfig: 黑白名单通话规则加载完成"); + } else { + LogUtils.e(TAG, "initRulesConfig: Rules实例获取失败,通话规则加载失败"); + } + } + + /** + * 初始化广播接收器(监听系统事件,如开机启动、网络变化) + */ private void initMainReceiver() { if (mMainReceiver == null) { mMainReceiver = new MainReceiver(this); mMainReceiver.registerAction(this); - LogUtils.d(TAG, "initMainReceiver: 广播接收器已注册"); + LogUtils.d(TAG, "initMainReceiver: 全局广播接收器注册完成"); + } else { + LogUtils.d(TAG, "initMainReceiver: 广播接收器已初始化,跳过重复注册"); + } + } + + /** + * 启动通话相关服务(通话监听+通话筛选,核心功能服务) + */ + private void startCallRelatedServices() { + // 启动通话监听服务 + try { + Intent callListenerIntent = new Intent(this, CallListenerService.class); + startService(callListenerIntent); + LogUtils.d(TAG, "startCallRelatedServices: CallListenerService启动成功"); + } catch (Exception e) { + LogUtils.e(TAG, "startCallRelatedServices: CallListenerService启动失败", e); + } + + // 启动通话筛选服务(API10+生效,低版本兼容) + try { + Intent screeningIntent = new Intent(this, MyCallScreeningService.class); + startService(screeningIntent); + LogUtils.d(TAG, "startCallRelatedServices: MyCallScreeningService启动成功"); + } catch (Exception e) { + LogUtils.e(TAG, "startCallRelatedServices: MyCallScreeningService启动失败", e); } } } diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java index 4d390c2..95a0884 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/MyCallScreeningService.java @@ -1,147 +1,237 @@ package cc.winboll.studio.contacts.services; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; import android.content.Context; +import android.content.Intent; import android.net.Uri; import android.os.Build; import android.telecom.CallScreeningService; import android.telephony.TelephonyManager; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; -import cc.winboll.studio.contacts.R; +import cc.winboll.studio.contacts.model.MainServiceBean; import cc.winboll.studio.libappbase.LogUtils; /** * @Author ZhanGSKen&豆包大模型 - * @Date 2025/12/12 19:00 - * @Describe 通话筛选服务(完全匹配 API 30 父类方法原型,严格 Java 7 兼容) - * 修复:1. 前台服务启动超时异常 2. 移除所有 Build.VERSION_CODES 高版本依赖 3. 强化空指针防护 + * @Describe 通话筛选服务(无前台服务),负责识别通话类型、拦截指定号码、处理正常通话逻辑 + * 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 强化空指针防护 | 无冗余代码 */ -@RequiresApi(api = 29) // API 29 = Android 10 = Build.VERSION_CODES.Q,直接写数值避免依赖 +@RequiresApi(api = 29) // 父类 CallScreeningService 最低要求 API29,明确标注 public class MyCallScreeningService extends CallScreeningService { - // ====================== 常量定义区(全硬编码,杜绝高版本 API 依赖) ====================== + + // ====================== 常量定义区(全硬编码,无高版本API依赖) ====================== public static final String TAG = "MyCallScreeningService"; - // 前台服务配置 - private static final int FOREGROUND_NOTIFICATION_ID = 1003; - private static final String FOREGROUND_CHANNEL_ID = "call_screening_service_channel"; - // API 版本常量(直接写数值,替代 Build.VERSION_CODES) - private static final int ANDROID_8_API = 26; // Android 8.0,通知渠道最低版本 - private static final int ANDROID_12_API = 31; // Android 12,stopForeground 带参数版本 - // stopForeground 参数常量(硬编码 1,替代 STOP_FOREGROUND_REMOVE) - private static final int STOP_FOREGROUND_REMOVE = 1; - // 通话方向常量(直接写数值,替代 Call.Details 中的常量,适配 API 30) - private static final int CALL_DIRECTION_INCOMING = 1; - private static final int CALL_DIRECTION_OUTGOING = 2; - // 成员变量 - private Context mContext; + // 通话方向常量(硬编码替代 Call.Details 高版本字段,适配API29-30) + private static final int CALL_DIRECTION_INCOMING = 1; // 来电 + private static final int CALL_DIRECTION_OUTGOING = 2; // 外拨 - // ====================== Service 生命周期方法 ====================== + // ====================== 成员属性区(精简必要属性,命名规范) ====================== + private Context mContext; // 上下文对象,避免重复调用 getApplicationContext() + + // ====================== Service生命周期方法区(按执行顺序排列) ====================== @Override public void onCreate() { super.onCreate(); mContext = this; - LogUtils.d(TAG, "onCreate: 通话筛选服务启动"); - - // ========== 核心优化:优先启动前台服务,放在 onCreate 最前端,避免超时 ========== - try { - Notification foregroundNotification = createForegroundNotification(); - startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification); - LogUtils.d(TAG, "onCreate: 前台服务启动成功"); - } catch (Exception e) { - LogUtils.e(TAG, "onCreate: 前台服务启动失败", e); - stopSelf(); // 启动失败直接停止服务,避免系统抛出异常 - return; - } + LogUtils.d(TAG, "===== onCreate: 通话筛选服务启动 ====="); } - /** - * 100% 匹配 API 30 CallScreeningService 抽象方法原型 - * 完全移除高版本依赖,使用硬编码常量 - */ @Override - @RequiresApi(api = 29) // 与类注解一致,明确最低 API 要求 - public void onScreenCall(@NonNull android.telecom.Call.Details details) { - // 1. 获取通话号码:多层空指针防护,Java 7 规范写法 - String phoneNumber = "未知号码"; - Uri handle = details.getHandle(); - if (handle != null) { - String schemePart = handle.getSchemeSpecificPart(); - if (schemePart != null && !schemePart.trim().isEmpty()) { - phoneNumber = schemePart.trim(); - } - } + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId); - // 2. 判断通话方向:用硬编码常量替代 API 高版本字段 - String callTypeStr; - int callType; - int direction = details.getCallDirection(); - if (direction == CALL_DIRECTION_INCOMING) { - callTypeStr = "来电"; - callType = TelephonyManager.CALL_STATE_RINGING; - } else if (direction == CALL_DIRECTION_OUTGOING) { - callTypeStr = "外拨"; - callType = TelephonyManager.CALL_STATE_OFFHOOK; - } else { - callTypeStr = "未知类型"; - callType = TelephonyManager.CALL_STATE_IDLE; - } + // 加载服务配置,决定重启策略(启用则自动重启,禁用则默认) + MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class); + int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId); + LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式")); - // Java 7 字符串拼接,避免 String.format 兼容性问题 - LogUtils.d(TAG, "检测到" + callTypeStr + ":" + phoneNumber); - - // 3. 自定义拦截逻辑:示例拦截 10086 - boolean shouldBlock = "10086".equals(phoneNumber); - - // 4. 构建筛选响应:Java 7 分步调用,不使用链式写法 - CallResponse.Builder responseBuilder = new CallResponse.Builder(); - responseBuilder.setDisallowCall(shouldBlock); - responseBuilder.setRejectCall(shouldBlock); - responseBuilder.setSkipCallLog(!shouldBlock); - responseBuilder.setSkipNotification(!shouldBlock); - CallResponse response = responseBuilder.build(); - - // 5. 提交筛选结果(API 30 父类方法,直接调用) - respondToCall(details, response); - - // 6. 处理业务逻辑 - if (!shouldBlock) { - handleNormalCall(phoneNumber, callType); - } else { - LogUtils.d(TAG, "已拦截通话:" + phoneNumber); - } + return startMode; } @Override public void onDestroy() { super.onDestroy(); - LogUtils.d(TAG, "onDestroy: 通话筛选服务销毁"); - // 适配 Android 12+ stopForeground 参数,硬编码常量避免依赖 - if (Build.VERSION.SDK_INT >= ANDROID_12_API) { - stopForeground(STOP_FOREGROUND_REMOVE); + // 置空上下文,释放引用,避免内存泄漏 + mContext = null; + LogUtils.d(TAG, "===== onDestroy: 通话筛选服务销毁完成 ====="); + } + + // ====================== 核心通话筛选方法区(父类抽象方法实现) ====================== + /** + * 核心:通话筛选入口(API29-30标准方法,100%兼容) + * 功能:识别通话号码/类型、拦截指定号码、处理正常通话 + */ + @Override + @RequiresApi(api = 29) + public void onScreenCall(@NonNull android.telecom.Call.Details details) { + LogUtils.d(TAG, "===== onScreenCall: 开始筛选通话 ====="); + + // 1. 安全获取通话号码(多层空指针防护,Java7规范写法) + String phoneNumber = getSafePhoneNumber(details); + // 2. 识别通话方向(来电/外拨/未知) + int callDirection = details.getCallDirection(); + String callTypeDesc = getCallDirectionDesc(callDirection); + int callState = getCallStateByDirection(callDirection); + + LogUtils.d(TAG, "筛选结果:通话类型=" + callTypeDesc + ",号码=" + phoneNumber); + + // 3. 自定义拦截逻辑(示例:拦截指定号码10086,可按需扩展黑白名单) + boolean isNeedBlock = isTargetBlockNumber(phoneNumber); + + // 4. 构建筛选响应(Java7分步调用,不使用链式写法,避免兼容问题) + CallResponse callResponse = buildCallScreeningResponse(isNeedBlock); + + // 5. 提交筛选结果(必须调用父类方法,完成拦截/放行逻辑) + respondToCall(details, callResponse); + + // 6. 分场景处理后续逻辑(拦截日志/正常通话业务) + handleCallAfterScreening(phoneNumber, callTypeDesc, isNeedBlock); + + LogUtils.d(TAG, "===== onScreenCall: 通话筛选完成 ====="); + } + + // ====================== 业务逻辑方法区(按功能拆分,低耦合) ====================== + /** + * 安全获取通话号码(多层空指针+空字符串防护,避免崩溃) + */ + private String getSafePhoneNumber(android.telecom.Call.Details details) { + String phoneNumber = "未知号码"; + if (details == null) { + LogUtils.w(TAG, "getSafePhoneNumber: 通话详情为空,无法获取号码"); + return phoneNumber; + } + + Uri handle = details.getHandle(); + if (handle != null) { + String schemePart = handle.getSchemeSpecificPart(); + if (schemePart != null && !schemePart.trim().isEmpty()) { + phoneNumber = schemePart.trim(); + LogUtils.d(TAG, "getSafePhoneNumber: 成功获取号码,原始值=" + schemePart + ",处理后=" + phoneNumber); + } else { + LogUtils.w(TAG, "getSafePhoneNumber: 号码格式异常,schemePart=" + schemePart); + } + } else { + LogUtils.w(TAG, "getSafePhoneNumber: 通话 handle 为空,无法获取号码"); + } + return phoneNumber; + } + + /** + * 判断是否为目标拦截号码(可扩展黑白名单逻辑,当前示例拦截10086) + */ + private boolean isTargetBlockNumber(String phoneNumber) { + if (phoneNumber == null || phoneNumber.trim().isEmpty()) { + LogUtils.w(TAG, "isTargetBlockNumber: 号码为空,不拦截"); + return false; + } + + // 示例拦截逻辑:拦截 10086(实际可替换为黑白名单查询) + boolean isBlock = "10086".equals(phoneNumber.trim()); + if (isBlock) { + LogUtils.d(TAG, "isTargetBlockNumber: 命中拦截规则,号码=" + phoneNumber); + } else { + LogUtils.d(TAG, "isTargetBlockNumber: 未命中拦截规则,号码=" + phoneNumber); + } + return isBlock; + } + + /** + * 构建通话筛选响应(按需配置拦截/放行参数,适配API29-30) + */ + private CallResponse buildCallScreeningResponse(boolean isNeedBlock) { + CallResponse.Builder responseBuilder = new CallResponse.Builder(); + // 拦截配置:是否禁止通话+是否拒绝通话(两者配合实现拦截) + responseBuilder.setDisallowCall(isNeedBlock); + responseBuilder.setRejectCall(isNeedBlock); + // 日志/通知配置:拦截的通话跳过日志和通知,正常通话保留 + responseBuilder.setSkipCallLog(isNeedBlock); + responseBuilder.setSkipNotification(isNeedBlock); + + CallResponse response = responseBuilder.build(); + LogUtils.d(TAG, "buildCallScreeningResponse: 响应构建完成,拦截状态=" + isNeedBlock); + return response; + } + + /** + * 筛选后分场景处理(拦截日志/正常通话业务扩展) + */ + private void handleCallAfterScreening(String phoneNumber, String callTypeDesc, boolean isNeedBlock) { + if (isNeedBlock) { + // 拦截场景:仅打日志(可扩展:添加拦截记录、本地存储等) + LogUtils.d(TAG, "handleCallAfterScreening: 已拦截通话,类型=" + callTypeDesc + ",号码=" + phoneNumber); + } else { + // 正常通话场景:处理业务逻辑(可扩展:通话记录、广播通知、号码识别等) + int callState = getCallStateByDirection(getCallDirectionFromDesc(callTypeDesc)); + handleNormalCallBusiness(phoneNumber, callState); } } - // ====================== 业务逻辑方法 ====================== /** - * 处理正常通话的扩展逻辑,Java 7 规范写法,添加空指针防护 + * 正常通话业务处理(核心业务扩展入口,强化空指针防护) */ - private void handleNormalCall(String phoneNumber, int callType) { + private void handleNormalCallBusiness(String phoneNumber, int callState) { if (phoneNumber == null || phoneNumber.trim().isEmpty()) { - LogUtils.w(TAG, "handleNormalCall: 号码为空,跳过处理"); + LogUtils.w(TAG, "handleNormalCallBusiness: 号码为空,跳过业务处理"); return; } - LogUtils.d(TAG, "处理正常通话:" + phoneNumber + " | 状态:" + getCallStateStr(callType)); - // 可在此添加通话记录、广播通知等逻辑 + + String callStateDesc = getCallStateDesc(callState); + LogUtils.d(TAG, "handleNormalCallBusiness: 处理正常通话业务,号码=" + phoneNumber + ",状态=" + callStateDesc); + + // 此处可扩展业务逻辑(示例): + // 1. 保存通话记录到本地 + // 2. 发送广播通知其他组件(如通话监听服务) + // 3. 调用号码识别接口,匹配联系人信息 + } + + // ====================== 工具辅助方法区(统一归类,复用性强) ====================== + /** + * 通话方向转文字描述(便于日志查看,快速定位场景) + */ + private String getCallDirectionDesc(int callDirection) { + switch (callDirection) { + case CALL_DIRECTION_INCOMING: + return "来电"; + case CALL_DIRECTION_OUTGOING: + return "外拨"; + default: + return "未知通话"; + } } /** - * 辅助方法:将通话状态转为字符串,避免重复代码 + * 文字描述转通话方向(配合业务逻辑反向匹配,避免重复判断) */ - private String getCallStateStr(int callType) { - switch (callType) { + private int getCallDirectionFromDesc(String callTypeDesc) { + if ("来电".equals(callTypeDesc)) { + return CALL_DIRECTION_INCOMING; + } else if ("外拨".equals(callTypeDesc)) { + return CALL_DIRECTION_OUTGOING; + } else { + return -1; // 未知方向 + } + } + + /** + * 通话方向转 TelephonyManager 状态(统一状态标准,便于业务复用) + */ + private int getCallStateByDirection(int callDirection) { + switch (callDirection) { + case CALL_DIRECTION_INCOMING: + return TelephonyManager.CALL_STATE_RINGING; // 来电=响铃中 + case CALL_DIRECTION_OUTGOING: + return TelephonyManager.CALL_STATE_OFFHOOK; // 外拨=通话中 + default: + return TelephonyManager.CALL_STATE_IDLE; // 未知=空闲 + } + } + + /** + * TelephonyManager 状态转文字描述(统一日志格式,提升可读性) + */ + private String getCallStateDesc(int callState) { + switch (callState) { case TelephonyManager.CALL_STATE_RINGING: return "响铃中"; case TelephonyManager.CALL_STATE_OFFHOOK: @@ -149,53 +239,8 @@ public class MyCallScreeningService extends CallScreeningService { case TelephonyManager.CALL_STATE_IDLE: return "空闲"; default: - return "未知"; + return "未知状态"; } } - - // ====================== 前台服务通知构建方法 ====================== - /** - * 创建前台服务通知:严格 Java 7 分步构建,强化异常防护 - * @return 可用的 Notification 实例 - */ - private Notification createForegroundNotification() { - // 1. 创建 Android 8.0+ 通知渠道,添加空指针防护 - if (Build.VERSION.SDK_INT >= ANDROID_8_API) { - NotificationChannel channel = new NotificationChannel( - FOREGROUND_CHANNEL_ID, - "来电筛查服务", - NotificationManager.IMPORTANCE_LOW - ); - channel.setDescription("后台处理来电筛查,防止服务被系统回收"); - // 关闭通知音效和震动,避免打扰用户 - channel.setSound(null, null); - channel.enableVibration(false); - - NotificationManager manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - if (manager != null) { - manager.createNotificationChannel(channel); - LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功"); - } else { - LogUtils.e(TAG, "createForegroundNotification: NotificationManager 获取失败"); - } - } - - // 2. 构建 Notification:Java 7 分步设置,不使用链式调用 - Notification.Builder builder; - if (Build.VERSION.SDK_INT >= ANDROID_8_API) { - builder = new Notification.Builder(mContext, FOREGROUND_CHANNEL_ID); - } else { - builder = new Notification.Builder(mContext); - } - - // 关键:确保小图标资源存在,替换为项目实际图标 - builder.setSmallIcon(R.drawable.ic_launcher); - builder.setContentTitle("来电筛查服务运行中"); - builder.setContentText("正在监控来电状态"); - builder.setPriority(Notification.PRIORITY_LOW); - builder.setOngoing(true); // 前台服务通知不可手动取消 - - return builder.build(); - } }