20251213_150313_142
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -98,19 +98,22 @@
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<!-- 辅助服务:dataSync 类型 -->
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
|
||||
<!-- 通话UI服务(系统绑定) -->
|
||||
<service
|
||||
android:name=".phonecallui.PhoneCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<meta-data
|
||||
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||
@@ -127,7 +130,7 @@
|
||||
android:name=".listenphonecall.CallListenerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall">
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name=".service.CallShowService"/>
|
||||
@@ -137,15 +140,16 @@
|
||||
<!-- API 30+ 通话筛选服务(替代 PROCESS_OUTGOING_CALLS 权限) -->
|
||||
<service
|
||||
android:name=".services.MyCallScreeningService"
|
||||
android:foregroundServiceType="phoneCall"
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true">
|
||||
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.CallScreeningService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".receivers.MainReceiver">
|
||||
<receiver android:name=".receivers.MainReceiver"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||
</intent-filter>
|
||||
@@ -153,7 +157,8 @@
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.APPStatusWidget"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:stopWithTask="false">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
@@ -167,7 +172,8 @@
|
||||
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener">
|
||||
<receiver android:name=".widgets.APPStatusWidgetClickListener"
|
||||
android:stopWithTask="false">
|
||||
<intent-filter>
|
||||
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||
</intent-filter>
|
||||
|
||||
@@ -41,39 +41,38 @@ import java.util.List;
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @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<PhoneConnectRuleBean> mRuleList; // 通话规则列表
|
||||
private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器
|
||||
|
||||
// ====================== 数据与业务参数区 ======================
|
||||
private int mnStreamMaxVolume;
|
||||
private int mnStreamVolume;
|
||||
private PhoneConnectRuleAdapter adapter;
|
||||
private List<PhoneConnectRuleBean> 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: 云盾信息控件未初始化,刷新失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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&豆包大模型<zhangsken@qq.com>
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user