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