20251213_134317_469
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Dec 13 03:37:48 GMT 2025
|
#Sat Dec 13 05:41:14 GMT 2025
|
||||||
stageCount=1
|
stageCount=1
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.12
|
baseVersion=15.12
|
||||||
publishVersion=15.12.0
|
publishVersion=15.12.0
|
||||||
buildCount=81
|
buildCount=99
|
||||||
baseBetaVersion=15.12.1
|
baseBetaVersion=15.12.1
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ public class App extends GlobalApplication {
|
|||||||
super.onCreate();
|
super.onCreate();
|
||||||
// 设置应用调试标志
|
// 设置应用调试标志
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
|
||||||
// 初始化窗口管理类
|
// 初始化窗口管理类
|
||||||
WinBoLLActivityManager.init(this);
|
WinBoLLActivityManager.init(this);
|
||||||
// 初始化 Toast 框架
|
// 初始化 Toast 框架
|
||||||
ToastUtils.init(this);
|
ToastUtils.init(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTerminate() {
|
||||||
|
super.onTerminate();
|
||||||
|
ToastUtils.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import cc.winboll.studio.contacts.dun.Rules;
|
|||||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||||
import cc.winboll.studio.contacts.fragments.ContactsFragment;
|
import cc.winboll.studio.contacts.fragments.ContactsFragment;
|
||||||
import cc.winboll.studio.contacts.fragments.LogFragment;
|
import cc.winboll.studio.contacts.fragments.LogFragment;
|
||||||
|
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||||
|
import cc.winboll.studio.contacts.services.MainService;
|
||||||
import cc.winboll.studio.contacts.utils.PermissionUtils;
|
import cc.winboll.studio.contacts.utils.PermissionUtils;
|
||||||
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
||||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
@@ -111,6 +113,17 @@ public final class MainActivity extends WinBollActivity implements IWinBoLLActiv
|
|||||||
|
|
||||||
// 直接初始化UI(原权限检查逻辑注释保留,按需启用)
|
// 直接初始化UI(原权限检查逻辑注释保留,按需启用)
|
||||||
initUIAndLogic(savedInstanceState);
|
initUIAndLogic(savedInstanceState);
|
||||||
|
|
||||||
|
MainServiceBean mainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||||
|
if (mainServiceBean != null && mainServiceBean.isEnable()) {
|
||||||
|
Intent intent = new Intent(this, MainService.class);
|
||||||
|
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
startForegroundService(intent);
|
||||||
|
} else {
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
LogUtils.d(TAG, "===== onCreate: 主Activity创建流程结束 =====");
|
LogUtils.d(TAG, "===== onCreate: 主Activity创建流程结束 =====");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,32 @@
|
|||||||
package cc.winboll.studio.contacts.phonecallui;
|
package cc.winboll.studio.contacts.phonecallui;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.MediaRecorder;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.CallLog;
|
|
||||||
import android.telecom.Call;
|
import android.telecom.Call;
|
||||||
import android.telecom.InCallService;
|
import android.telecom.InCallService;
|
||||||
import android.telephony.TelephonyManager;
|
|
||||||
|
|
||||||
import cc.winboll.studio.contacts.ActivityStack;
|
import cc.winboll.studio.contacts.ActivityStack;
|
||||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
|
||||||
import cc.winboll.studio.contacts.dun.Rules;
|
import cc.winboll.studio.contacts.dun.Rules;
|
||||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||||
|
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
* @Date 2025/02/13 06:58:04
|
* @Date 2025/02/13 06:58:04
|
||||||
* @Describe 通话状态监听服务(需 Android 6.0+),负责通话状态回调、录音、铃音控制及黑白名单校验
|
* @Describe 通话状态监听服务(需 Android 6.0+),负责通话状态回调、铃音控制及黑白名单校验
|
||||||
* @see PhoneCallActivity
|
* @see PhoneCallActivity
|
||||||
* @see android.telecom.InCallService
|
* @see android.telecom.InCallService
|
||||||
*/
|
*/
|
||||||
public class PhoneCallService extends InCallService {
|
public class PhoneCallService extends InCallService {
|
||||||
// ====================== 常量定义区 ======================
|
// ====================== 常量定义区 ======================
|
||||||
public static final String TAG = "PhoneCallService";
|
public static final String TAG = "PhoneCallService";
|
||||||
// 强制指定正确常量值,彻底解决冲突
|
// Call 通话状态(固定正确值,避免冲突)
|
||||||
// TelephonyManager 通话状态
|
private static final int CALL_STATE_IDLE = Call.STATE_NEW;
|
||||||
//private static final int CALL_STATE_OFFHOOK = TelephonyManager.CALL_STATE_OFFHOOK; // 固定值=2
|
private static final int CALL_STATE_RINGING = Call.STATE_RINGING; // 正确值=1
|
||||||
//private static final int CALL_STATE_IDLE = TelephonyManager.CALL_STATE_IDLE; // 固定值=0
|
private static final int CALL_STATE_CONNECTING = Call.STATE_CONNECTING; // 正确值=3
|
||||||
//private static final int CALL_STATE_RINGING_TELE = TelephonyManager.CALL_STATE_RINGING; // 固定值=1
|
private static final int CALL_STATE_ACTIVE = Call.STATE_ACTIVE; // 正确值=2
|
||||||
// Call 通话状态
|
private static final int CALL_STATE_DISCONNECTED = Call.STATE_DISCONNECTED; // 正确值=4
|
||||||
private static final int CALL_STATE_IDLE = Call.STATE_NEW;
|
|
||||||
private static final int CALL_STATE_RINGING = Call.STATE_RINGING; //正确值=1
|
|
||||||
private static final int CALL_STATE_CONNECTING = Call.STATE_CONNECTING; //正确值=3
|
|
||||||
private static final int CALL_STATE_ACTIVE = Call.STATE_ACTIVE; //正确值=2
|
|
||||||
private static final int CALL_STATE_DISCONNECTED = Call.STATE_DISCONNECTED; //正确值=4
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
// ====================== 成员变量区 ======================
|
||||||
private MediaRecorder mMediaRecorder;
|
|
||||||
private Call.Callback mCallCallback;
|
private Call.Callback mCallCallback;
|
||||||
|
|
||||||
// ====================== 内部枚举类 ======================
|
// ====================== 内部枚举类 ======================
|
||||||
@@ -101,12 +84,11 @@ public class PhoneCallService extends InCallService {
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
releaseRecorder();
|
|
||||||
CallLogFragment.updateCallLogFragment();
|
CallLogFragment.updateCallLogFragment();
|
||||||
LogUtils.d(TAG, "onDestroy: 通话服务已销毁,资源已释放");
|
LogUtils.d(TAG, "onDestroy: 通话服务已销毁,资源已释放");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 核心修复:通话状态回调 ======================
|
// ====================== 核心:通话状态回调 ======================
|
||||||
private void initCallCallback() {
|
private void initCallCallback() {
|
||||||
mCallCallback = new Call.Callback() {
|
mCallCallback = new Call.Callback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -115,29 +97,24 @@ public class PhoneCallService extends InCallService {
|
|||||||
LogUtils.d(TAG, "onStateChanged: 状态变更 | 状态码=" + state + " | 描述=" + getCallStateDesc(state));
|
LogUtils.d(TAG, "onStateChanged: 状态变更 | 状态码=" + state + " | 描述=" + getCallStateDesc(state));
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
// 仅合并值为2的两个状态,其他状态独立分支
|
// 通话活跃状态(仅监听,无录音)
|
||||||
case CALL_STATE_ACTIVE:
|
case CALL_STATE_ACTIVE:
|
||||||
//case CALL_STATE_OFFHOOK:
|
LogUtils.d(TAG, "onStateChanged: 通话已接通/活跃");
|
||||||
long callId = getCurrentCallId();
|
|
||||||
if (callId != -1) {
|
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话接通/活跃,启动录音 | 通话ID=" + callId);
|
|
||||||
startRecording(callId);
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "onStateChanged: 未获取到通话ID,无法录音");
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
// 状态码1独立分支,彻底解决重复问题
|
// 响铃状态
|
||||||
case CALL_STATE_RINGING:
|
case CALL_STATE_RINGING:
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话处于响铃状态");
|
LogUtils.d(TAG, "onStateChanged: 通话处于响铃状态");
|
||||||
break;
|
break;
|
||||||
|
// 空闲状态(通话挂断后)
|
||||||
case CALL_STATE_IDLE:
|
case CALL_STATE_IDLE:
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话挂断,停止录音");
|
LogUtils.d(TAG, "onStateChanged: 通话挂断");
|
||||||
stopRecording();
|
|
||||||
break;
|
break;
|
||||||
|
// 通话断开,关闭界面
|
||||||
case CALL_STATE_DISCONNECTED:
|
case CALL_STATE_DISCONNECTED:
|
||||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话断开,关闭通话界面");
|
LogUtils.d(TAG, "onStateChanged: 通话断开,关闭通话界面");
|
||||||
break;
|
break;
|
||||||
|
// 通话连接中
|
||||||
case CALL_STATE_CONNECTING:
|
case CALL_STATE_CONNECTING:
|
||||||
LogUtils.d(TAG, "onStateChanged: 通话正在连接");
|
LogUtils.d(TAG, "onStateChanged: 通话正在连接");
|
||||||
break;
|
break;
|
||||||
@@ -149,7 +126,7 @@ public class PhoneCallService extends InCallService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 核心业务方法区 ======================
|
// ====================== 核心业务方法区(铃音控制+黑白名单) ======================
|
||||||
private CallType judgeCallType(int callState) {
|
private CallType judgeCallType(int callState) {
|
||||||
switch (callState) {
|
switch (callState) {
|
||||||
case CALL_STATE_RINGING:
|
case CALL_STATE_RINGING:
|
||||||
@@ -232,90 +209,7 @@ public class PhoneCallService extends InCallService {
|
|||||||
LogUtils.d(TAG, "startPhoneCallActivity: 通话界面已启动");
|
LogUtils.d(TAG, "startPhoneCallActivity: 通话界面已启动");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 录音相关方法区 ======================
|
|
||||||
private void startRecording(long callId) {
|
|
||||||
releaseRecorder();
|
|
||||||
mMediaRecorder = new MediaRecorder();
|
|
||||||
try {
|
|
||||||
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.VOICE_CALL);
|
|
||||||
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
|
|
||||||
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
|
||||||
String path = getOutputFilePath(callId);
|
|
||||||
mMediaRecorder.setOutputFile(path);
|
|
||||||
mMediaRecorder.prepare();
|
|
||||||
mMediaRecorder.start();
|
|
||||||
LogUtils.d(TAG, "startRecording: 录音启动成功 | 路径=" + path);
|
|
||||||
} catch (IOException | IllegalStateException e) {
|
|
||||||
LogUtils.e(TAG, "startRecording: 录音启动失败", e);
|
|
||||||
releaseRecorder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopRecording() {
|
|
||||||
if (mMediaRecorder != null) {
|
|
||||||
try {
|
|
||||||
mMediaRecorder.stop();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
LogUtils.e(TAG, "stopRecording: 录音停止失败", e);
|
|
||||||
} finally {
|
|
||||||
releaseRecorder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOutputFilePath(long callId) {
|
|
||||||
File dir = getExternalFilesDir(TAG);
|
|
||||||
if (dir == null) {
|
|
||||||
dir = new File(getFilesDir(), TAG);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
dir.mkdirs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new File(dir, String.format("call_%d.mp4", callId)).getAbsolutePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseRecorder() {
|
|
||||||
if (mMediaRecorder != null) {
|
|
||||||
mMediaRecorder.release();
|
|
||||||
mMediaRecorder = null;
|
|
||||||
LogUtils.d(TAG, "releaseRecorder: 录音资源已释放");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 辅助工具方法区 ======================
|
// ====================== 辅助工具方法区 ======================
|
||||||
private long getCurrentCallId() {
|
|
||||||
ContentResolver resolver = getApplicationContext().getContentResolver();
|
|
||||||
if (resolver == null) {
|
|
||||||
LogUtils.w(TAG, "getCurrentCallId: 内容解析器为null");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = CallLog.Calls.CONTENT_URI;
|
|
||||||
String[] projection = {CallLog.Calls._ID};
|
|
||||||
String selection = CallLog.Calls.TYPE + " = " + CallLog.Calls.OUTGOING_TYPE
|
|
||||||
+ " OR " + CallLog.Calls.TYPE + " = " + CallLog.Calls.INCOMING_TYPE;
|
|
||||||
String sort = CallLog.Calls.DATE + " DESC";
|
|
||||||
Cursor cursor = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cursor = resolver.query(uri, projection, selection, null, sort);
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
long id = cursor.getLong(cursor.getColumnIndex(CallLog.Calls._ID));
|
|
||||||
LogUtils.d(TAG, "getCurrentCallId: 通话ID=" + id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
LogUtils.w(TAG, "getCurrentCallId: 未查询到通话记录");
|
|
||||||
return -1;
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, "getCurrentCallId: 查询失败(权限不足)", e);
|
|
||||||
return -1;
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCallStateDesc(int state) {
|
private String getCallStateDesc(int state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CALL_STATE_RINGING:
|
case CALL_STATE_RINGING:
|
||||||
@@ -324,8 +218,6 @@ public class PhoneCallService extends InCallService {
|
|||||||
return "连接中";
|
return "连接中";
|
||||||
case CALL_STATE_ACTIVE:
|
case CALL_STATE_ACTIVE:
|
||||||
return "活跃";
|
return "活跃";
|
||||||
//case CALL_STATE_OFFHOOK:
|
|
||||||
// return "摘机";
|
|
||||||
case CALL_STATE_IDLE:
|
case CALL_STATE_IDLE:
|
||||||
return "空闲";
|
return "空闲";
|
||||||
case CALL_STATE_DISCONNECTED:
|
case CALL_STATE_DISCONNECTED:
|
||||||
@@ -334,5 +226,18 @@ public class PhoneCallService extends InCallService {
|
|||||||
return "未知状态";
|
return "未知状态";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================== 静态内部类:PhoneCallManager(避免编译报错) ======================
|
||||||
|
public static class PhoneCallManager {
|
||||||
|
public static Call sCurrentCall;
|
||||||
|
|
||||||
|
private PhoneCallManager() {
|
||||||
|
// 私有构造,禁止实例化
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Call getCurrentCall() {
|
||||||
|
return sCurrentCall;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,9 @@ import android.content.Intent;
|
|||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.Looper;
|
|
||||||
|
|
||||||
import cc.winboll.studio.contacts.R;
|
import cc.winboll.studio.contacts.R;
|
||||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||||
import cc.winboll.studio.contacts.utils.AppForegroundUtils;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,7 +155,7 @@ public class AssistantService extends Service {
|
|||||||
LogUtils.d(TAG, "onCreate: 守护服务创建");
|
LogUtils.d(TAG, "onCreate: 守护服务创建");
|
||||||
|
|
||||||
// 适配 Android 12+ 后台启动限制:应用后台时启动为前台服务
|
// 适配 Android 12+ 后台启动限制:应用后台时启动为前台服务
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) {
|
if (Build.VERSION.SDK_INT >= ANDROID_12_API) {
|
||||||
Notification notification = createForegroundNotification();
|
Notification notification = createForegroundNotification();
|
||||||
// 修复:使用 dataSync 类型,添加异常捕获防止崩溃
|
// 修复:使用 dataSync 类型,添加异常捕获防止崩溃
|
||||||
try {
|
try {
|
||||||
@@ -256,7 +252,7 @@ public class AssistantService extends Service {
|
|||||||
|
|
||||||
Intent intent = new Intent(this, MainService.class);
|
Intent intent = new Intent(this, MainService.class);
|
||||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) {
|
if (Build.VERSION.SDK_INT >= ANDROID_12_API) {
|
||||||
LogUtils.d(TAG, "wakeupAndBindMain: 应用后台,启动主服务为前台服务");
|
LogUtils.d(TAG, "wakeupAndBindMain: 应用后台,启动主服务为前台服务");
|
||||||
startForegroundService(intent);
|
startForegroundService(intent);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import cc.winboll.studio.contacts.listenphonecall.CallListenerService;
|
|||||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||||
import cc.winboll.studio.contacts.receivers.MainReceiver;
|
import cc.winboll.studio.contacts.receivers.MainReceiver;
|
||||||
import cc.winboll.studio.contacts.threads.MainServiceThread;
|
|
||||||
import cc.winboll.studio.contacts.utils.AppForegroundUtils;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
@@ -34,7 +32,7 @@ import java.util.TimerTask;
|
|||||||
* @Date 2025/02/13 06:56:41
|
* @Date 2025/02/13 06:56:41
|
||||||
* @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动
|
* @Describe 拨号主服务,负责核心业务逻辑、守护进程绑定、铃声音量监控及通话监听启动
|
||||||
* 严格适配 Android API 30 + Java 7 语法规范 | 解决前台服务启动超时崩溃
|
* 严格适配 Android API 30 + Java 7 语法规范 | 解决前台服务启动超时崩溃
|
||||||
* 新增:统一在 mainService() 中启动电话监听服务 + 通话筛选服务
|
* 优化:移除延迟启动,非核心服务直接在 mainService() 中启动
|
||||||
*/
|
*/
|
||||||
public class MainService extends Service {
|
public class MainService extends Service {
|
||||||
// ====================== 常量定义区(全硬编码,杜绝高版本 API 依赖) ======================
|
// ====================== 常量定义区(全硬编码,杜绝高版本 API 依赖) ======================
|
||||||
@@ -52,9 +50,8 @@ public class MainService extends Service {
|
|||||||
private static final int ANDROID_8_API = 26;
|
private static final int ANDROID_8_API = 26;
|
||||||
private static final int ANDROID_10_API = 29;
|
private static final int ANDROID_10_API = 29;
|
||||||
private static final int ANDROID_12_API = 31;
|
private static final int ANDROID_12_API = 31;
|
||||||
// 重试延迟时间 & 非核心服务延迟启动时间
|
// 重试延迟时间(仅保留守护服务重绑定延迟,移除非核心服务延迟)
|
||||||
private static final long RETRY_DELAY_MS = 3000L;
|
private static final long RETRY_DELAY_MS = 3000L;
|
||||||
private static final long DELAY_NON_CORE_SERVICE_MS = 100L;
|
|
||||||
|
|
||||||
// ====================== 静态成员变量区 ======================
|
// ====================== 静态成员变量区 ======================
|
||||||
private static MainService sMainServiceInstance;
|
private static MainService sMainServiceInstance;
|
||||||
@@ -63,14 +60,13 @@ public class MainService extends Service {
|
|||||||
// ====================== 成员变量区 ======================
|
// ====================== 成员变量区 ======================
|
||||||
private volatile boolean mIsServiceRunning;
|
private volatile boolean mIsServiceRunning;
|
||||||
private MainServiceBean mMainServiceBean;
|
private MainServiceBean mMainServiceBean;
|
||||||
private MainServiceThread mMainServiceThread;
|
//private MainServiceThread mMainServiceThread;
|
||||||
private MainServiceHandler mMainServiceHandler;
|
private MainServiceHandler mMainServiceHandler;
|
||||||
private MyServiceConnection mMyServiceConnection;
|
private MyServiceConnection mMyServiceConnection;
|
||||||
private AssistantService mAssistantService;
|
private AssistantService mAssistantService;
|
||||||
private boolean mIsBound;
|
private boolean mIsBound;
|
||||||
private MainReceiver mMainReceiver;
|
private MainReceiver mMainReceiver;
|
||||||
private Timer mStreamVolumeCheckTimer;
|
private Timer mStreamVolumeCheckTimer;
|
||||||
private Handler mDelayHandler;
|
|
||||||
|
|
||||||
// ====================== Binder 内部类 ======================
|
// ====================== Binder 内部类 ======================
|
||||||
public class MyBinder extends Binder {
|
public class MyBinder extends Binder {
|
||||||
@@ -147,13 +143,7 @@ public class MainService extends Service {
|
|||||||
}
|
}
|
||||||
LogUtils.d(TAG, "startMainService: 执行启动主服务操作");
|
LogUtils.d(TAG, "startMainService: 执行启动主服务操作");
|
||||||
Intent intent = new Intent(context, MainService.class);
|
Intent intent = new Intent(context, MainService.class);
|
||||||
// API 30 属于 Android 10+,按前台服务逻辑处理
|
context.startForegroundService(intent);
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API && !AppForegroundUtils.isAppForeground(context)) {
|
|
||||||
LogUtils.d(TAG, "startMainService: 应用后台,使用 startForegroundService 启动");
|
|
||||||
context.startForegroundService(intent);
|
|
||||||
} else {
|
|
||||||
context.startService(intent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void restartMainService(Context context) {
|
public static void restartMainService(Context context) {
|
||||||
@@ -194,6 +184,7 @@ public class MainService extends Service {
|
|||||||
MainServiceBean bean = new MainServiceBean();
|
MainServiceBean bean = new MainServiceBean();
|
||||||
bean.setIsEnable(true);
|
bean.setIsEnable(true);
|
||||||
MainServiceBean.saveBean(context, bean);
|
MainServiceBean.saveBean(context, bean);
|
||||||
|
stopMainService(context);
|
||||||
startMainService(context);
|
startMainService(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,52 +204,6 @@ public class MainService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务启动前置校验
|
|
||||||
*/
|
|
||||||
private boolean canStartService(Class<?> serviceClass) {
|
|
||||||
// 服务未启用/已运行 → 禁止启动
|
|
||||||
if (mMainServiceBean == null || !mMainServiceBean.isEnable()) {
|
|
||||||
LogUtils.w(TAG, "canStartService: 主服务未启用,禁止启动 " + serviceClass.getSimpleName());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isServiceRunning(serviceClass)) {
|
|
||||||
LogUtils.d(TAG, "canStartService: " + serviceClass.getSimpleName() + " 已运行,跳过启动");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Android 12+ 应用后台 → 禁止启动非核心服务
|
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_12_API && !AppForegroundUtils.isAppForeground(this)) {
|
|
||||||
LogUtils.w(TAG, "canStartService: Android 12+ 应用后台,禁止启动 " + serviceClass.getSimpleName());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 延迟启动非核心服务
|
|
||||||
*/
|
|
||||||
private void delayStartNonCoreService(final Class<?> serviceClass) {
|
|
||||||
if (!canStartService(serviceClass)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mDelayHandler.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Intent intent = new Intent(MainService.this, serviceClass);
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API && !AppForegroundUtils.isAppForeground(MainService.this)) {
|
|
||||||
startForegroundService(intent);
|
|
||||||
} else {
|
|
||||||
startService(intent);
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "delayStartNonCoreService: " + serviceClass.getSimpleName() + " 启动成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "delayStartNonCoreService: " + serviceClass.getSimpleName() + " 启动失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, DELAY_NON_CORE_SERVICE_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建前台服务通知
|
* 创建前台服务通知
|
||||||
*/
|
*/
|
||||||
@@ -299,25 +244,6 @@ public class MainService extends Service {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动电话监听服务(原有:CallListenerService)
|
|
||||||
*/
|
|
||||||
private void startPhoneCallListener() {
|
|
||||||
delayStartNonCoreService(CallListenerService.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增:启动通话筛选服务(MyCallScreeningService,API 10+ 生效)
|
|
||||||
*/
|
|
||||||
private void startCallScreeningService() {
|
|
||||||
// 通话筛选服务仅 Android 10+(API 29+)支持,低版本跳过
|
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
|
||||||
delayStartNonCoreService(MyCallScreeningService.class);
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "startCallScreeningService: 系统版本低于 Android 10,不支持通话筛选服务,跳过启动");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查服务是否正在运行
|
* 检查服务是否正在运行
|
||||||
*/
|
*/
|
||||||
@@ -349,15 +275,17 @@ public class MainService extends Service {
|
|||||||
sMainServiceInstance = this;
|
sMainServiceInstance = this;
|
||||||
mIsServiceRunning = false;
|
mIsServiceRunning = false;
|
||||||
|
|
||||||
// 仅初始化核心组件
|
// 仅初始化核心组件(移除 mDelayHandler 初始化,无需延迟启动)
|
||||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||||
mMyServiceConnection = new MyServiceConnection();
|
mMyServiceConnection = new MyServiceConnection();
|
||||||
mMainServiceHandler = new MainServiceHandler(this);
|
mMainServiceHandler = new MainServiceHandler(this);
|
||||||
mDelayHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
// 初始化铃声音量检查定时器
|
// 初始化铃声音量检查定时器
|
||||||
initVolumeCheckTimer();
|
initVolumeCheckTimer();
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
mainService();
|
||||||
|
|
||||||
LogUtils.d(TAG, "===== onCreate: 主服务创建完成(仅初始化组件) =====");
|
LogUtils.d(TAG, "===== onCreate: 主服务创建完成(仅初始化组件) =====");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,19 +308,10 @@ public class MainService extends Service {
|
|||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁(强制清理所有资源) =====");
|
LogUtils.d(TAG, "===== onDestroy: 主服务开始销毁(强制清理所有资源) =====");
|
||||||
|
|
||||||
// 1. 强制标记服务为未运行
|
// 2. 释放定时器资源
|
||||||
mIsServiceRunning = false;
|
|
||||||
|
|
||||||
// 2. 释放延迟 Handler 资源
|
|
||||||
if (mDelayHandler != null) {
|
|
||||||
mDelayHandler.removeCallbacksAndMessages(null);
|
|
||||||
mDelayHandler = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 释放定时器资源
|
|
||||||
cancelVolumeCheckTimer();
|
cancelVolumeCheckTimer();
|
||||||
|
|
||||||
// 4. 强制解除守护服务绑定
|
// 3. 强制解除守护服务绑定
|
||||||
if (mIsBound && mMyServiceConnection != null) {
|
if (mIsBound && mMyServiceConnection != null) {
|
||||||
try {
|
try {
|
||||||
unbindService(mMyServiceConnection);
|
unbindService(mMyServiceConnection);
|
||||||
@@ -404,97 +323,101 @@ public class MainService extends Service {
|
|||||||
mMyServiceConnection = null;
|
mMyServiceConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 强制注销广播接收器
|
// 4. 强制注销广播接收器
|
||||||
if (mMainReceiver != null) {
|
if (mMainReceiver != null) {
|
||||||
mMainReceiver.unregisterAction(this);
|
mMainReceiver.unregisterAction(this);
|
||||||
mMainReceiver = null;
|
mMainReceiver = null;
|
||||||
LogUtils.d(TAG, "onDestroy: 广播接收器已注销");
|
LogUtils.d(TAG, "onDestroy: 广播接收器已注销");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 强制停止主业务线程
|
// 5. 强制停止主业务线程
|
||||||
if (mMainServiceThread != null) {
|
// if (mMainServiceThread != null) {
|
||||||
mMainServiceThread.setIsExit(true);
|
// mMainServiceThread.setIsExit(true);
|
||||||
mMainServiceThread = null;
|
// mMainServiceThread = null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 7. 强制停止所有子服务(新增:停止通话筛选服务 MyCallScreeningService)
|
// 6. 强制停止所有子服务
|
||||||
stopService(new Intent(this, AssistantService.class));
|
stopService(new Intent(this, AssistantService.class));
|
||||||
stopService(new Intent(this, CallListenerService.class));
|
stopService(new Intent(this, CallListenerService.class));
|
||||||
stopService(new Intent(this, MyCallScreeningService.class));
|
stopService(new Intent(this, MyCallScreeningService.class));
|
||||||
LogUtils.d(TAG, "onDestroy: 所有子服务已强制停止");
|
LogUtils.d(TAG, "onDestroy: 所有子服务已强制停止");
|
||||||
|
|
||||||
// 8. 清空静态实例,避免内存泄漏(不改动 Bean 配置)
|
// 7. 清空静态实例,避免内存泄漏(不改动 Bean 配置)
|
||||||
sMainServiceInstance = null;
|
sMainServiceInstance = null;
|
||||||
sTomCatInstance = null;
|
sTomCatInstance = null;
|
||||||
mMainServiceHandler = null;
|
mMainServiceHandler = null;
|
||||||
mAssistantService = null;
|
mAssistantService = null;
|
||||||
mMainServiceBean = null;
|
mMainServiceBean = null;
|
||||||
|
|
||||||
|
// 标记服务为未运行
|
||||||
|
mIsServiceRunning = false;
|
||||||
LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成(资源全清理,Bean 配置无改动) =====");
|
LogUtils.d(TAG, "===== onDestroy: 主服务销毁完成(资源全清理,Bean 配置无改动) =====");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 核心业务逻辑:优先判断运行状态 → 再校验配置启用状态 ======================
|
// ====================== 核心业务逻辑:非核心服务直接启动,无延迟 ======================
|
||||||
private void mainService() {
|
private synchronized void mainService() {
|
||||||
LogUtils.d(TAG, "mainService: 执行核心业务逻辑");
|
LogUtils.d(TAG, "mainService: 执行核心业务逻辑");
|
||||||
|
|
||||||
// 第一步:优先判断服务是否已运行,已运行则直接退出(不做任何动作)
|
// 第一步:优先判断服务是否已运行,已运行则直接退出(不做任何动作)
|
||||||
if (mIsServiceRunning) {
|
if (!mIsServiceRunning) {
|
||||||
LogUtils.d(TAG, "mainService: 服务已处于运行状态,直接退出");
|
mIsServiceRunning = true;
|
||||||
return;
|
// 第二步:加载最新配置,校验服务是否启用(未启用则退出)
|
||||||
}
|
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||||
|
if (mMainServiceBean == null && mMainServiceBean.isEnable()) {
|
||||||
|
// 第三步:配置启用且服务未运行,执行启动流程
|
||||||
|
LogUtils.i(TAG, "mainService: 服务未运行且配置已启用,开始启动核心流程");
|
||||||
|
|
||||||
// 第二步:加载最新配置,校验服务是否启用(未启用则退出)
|
// 1. 启动前台服务(优先执行,避免超时)
|
||||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
try {
|
||||||
if (mMainServiceBean == null || !mMainServiceBean.isEnable()) {
|
Notification foregroundNotification = createForegroundNotification();
|
||||||
LogUtils.w(TAG, "mainService: 服务配置未启用/配置加载失败,退出启动流程");
|
startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||||
return;
|
//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. 绑定守护服务
|
||||||
mIsServiceRunning = true;
|
wakeupAndBindAssistant();
|
||||||
LogUtils.i(TAG, "mainService: 服务未运行且配置已启用,开始启动核心流程");
|
|
||||||
|
|
||||||
// 1. 启动前台服务(优先执行,避免超时)
|
// 3. 初始化业务组件
|
||||||
try {
|
initTomCat();
|
||||||
Notification foregroundNotification = createForegroundNotification();
|
initMainReceiver();
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
Rules.getInstance(this).loadRules();
|
||||||
startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification, FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
LogUtils.d(TAG, "mainService: 黑白名单规则已加载");
|
||||||
LogUtils.d(TAG, "mainService: 主服务已启动为前台服务(dataSync 类型,API30+)");
|
|
||||||
} else {
|
|
||||||
startForeground(FOREGROUND_NOTIFICATION_ID, foregroundNotification);
|
|
||||||
LogUtils.d(TAG, "mainService: 主服务已启动为前台服务(低版本兼容)");
|
|
||||||
}
|
|
||||||
} 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. 绑定守护服务
|
// 4. 直接启动电话监听服务(移除延迟,校验通过则立即启动)
|
||||||
wakeupAndBindAssistant();
|
Intent callListenerIntent = new Intent(this, CallListenerService.class);
|
||||||
|
try {
|
||||||
|
startForegroundService(callListenerIntent);
|
||||||
|
//startService(callListenerIntent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "mainService: CallListenerService 启动失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 初始化业务组件
|
|
||||||
initTomCat();
|
|
||||||
initMainReceiver();
|
|
||||||
Rules.getInstance(this).loadRules();
|
|
||||||
LogUtils.d(TAG, "mainService: 黑白名单规则已加载");
|
|
||||||
|
|
||||||
// 4. 启动电话监听服务(原有:CallListenerService)
|
// 5. 直接启动通话筛选服务(API 10+ 生效,无延迟)
|
||||||
startPhoneCallListener();
|
Intent screeningIntent = new Intent(this, MyCallScreeningService.class);
|
||||||
|
try {
|
||||||
|
startForegroundService(screeningIntent);
|
||||||
|
//startService(screeningIntent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "mainService: MyCallScreeningService 启动失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 新增:启动通话筛选服务(MyCallScreeningService,API 10+ 生效)
|
// 6. 启动主业务线程
|
||||||
startCallScreeningService();
|
// mMainServiceThread = MainServiceThread.getInstance(this, mMainServiceHandler);
|
||||||
|
// mMainServiceThread.start();
|
||||||
// 6. 启动主业务线程
|
LogUtils.i(TAG, "mainService: 核心流程启动完成,主服务正常运行");
|
||||||
mMainServiceThread = MainServiceThread.getInstance(this, mMainServiceHandler);
|
}
|
||||||
mMainServiceThread.start();
|
}
|
||||||
LogUtils.i(TAG, "mainService: 核心流程启动完成,主服务正常运行");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void wakeupAndBindAssistant() {
|
private void wakeupAndBindAssistant() {
|
||||||
@@ -503,7 +426,7 @@ public class MainService extends Service {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Intent intent = new Intent(this, AssistantService.class);
|
Intent intent = new Intent(this, AssistantService.class);
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API && !AppForegroundUtils.isAppForeground(this)) {
|
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
||||||
LogUtils.d(TAG, "wakeupAndBindAssistant: 应用后台,启动 AssistantService 为前台服务");
|
LogUtils.d(TAG, "wakeupAndBindAssistant: 应用后台,启动 AssistantService 为前台服务");
|
||||||
startForegroundService(intent);
|
startForegroundService(intent);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.utils;
|
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/12/12 15:20
|
|
||||||
* @Describe AppForegroundUtils 应用前后台状态判断工具类(兼容 Java 7)
|
|
||||||
*/
|
|
||||||
public class AppForegroundUtils {
|
|
||||||
|
|
||||||
public static final String TAG = "AppForegroundUtils";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断应用是否处于前台状态
|
|
||||||
* @param context 上下文
|
|
||||||
* @return true-前台,false-后台/无上下文
|
|
||||||
*/
|
|
||||||
public static boolean isAppForeground(Context context) {
|
|
||||||
if (context == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
||||||
if (activityManager == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String packageName = context.getPackageName();
|
|
||||||
List<ActivityManager.RunningAppProcessInfo> processList = activityManager.getRunningAppProcesses();
|
|
||||||
if (processList == null || processList.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍历进程列表,匹配当前应用包名并判断前台状态
|
|
||||||
for (ActivityManager.RunningAppProcessInfo processInfo : processList) {
|
|
||||||
// Java 7 兼容:避免 Lambda,使用普通循环 + TextUtils 判等
|
|
||||||
if (!TextUtils.isEmpty(processInfo.processName)
|
|
||||||
&& TextUtils.equals(processInfo.processName, packageName)
|
|
||||||
&& processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user