diff --git a/contacts/build.properties b/contacts/build.properties index e3ca10c..363a895 100644 --- a/contacts/build.properties +++ b/contacts/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Thu Apr 09 02:36:26 HKT 2026 +#Sat Apr 18 08:39:12 GMT 2026 stageCount=10 libraryProject= baseVersion=15.14 publishVersion=15.14.9 -buildCount=0 +buildCount=25 baseBetaVersion=15.14.10 diff --git a/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml index a201cb5..3a2a347 100644 --- a/contacts/src/main/AndroidManifest.xml +++ b/contacts/src/main/AndroidManifest.xml @@ -9,54 +9,73 @@ - + + + - + - + + + + + - + - + - + + + + + - + - + + + + + + + - + + + + - + @@ -66,8 +85,11 @@ android:exported="true"> + + + @@ -77,6 +99,7 @@ android:label="CallActivity" android:launchMode="singleTask" android:exported="true"> + + + + + + + + + + - + android:stopWithTask="false"/> - + android:stopWithTask="false"/> - + android:stopWithTask="false"> + + - + + + - - + android:stopWithTask="false"> + + + + - + + + + + + android:stopWithTask="false"> + + + + - + + + + + + + - diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/activities/UnitTestActivity.java b/contacts/src/main/java/cc/winboll/studio/contacts/activities/UnitTestActivity.java index 7a60d28..8d3c2f5 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/activities/UnitTestActivity.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/activities/UnitTestActivity.java @@ -5,7 +5,9 @@ import android.view.View; import android.widget.EditText; import androidx.appcompat.app.AppCompatActivity; import cc.winboll.studio.contacts.R; +import cc.winboll.studio.contacts.activities.UnitTestActivity; import cc.winboll.studio.contacts.dun.Rules; +import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService; import cc.winboll.studio.contacts.utils.IntUtils; import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libappbase.LogUtils; @@ -116,6 +118,13 @@ public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivit LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed)); } LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成"); + + new Thread(new Runnable(){ + @Override + public void run() { + LimitedTimeSpecialChannelService.unitTest(UnitTestActivity.this); + } + }).start(); } // ====================== 私有工具函数区 ====================== diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java b/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java index 6a62579..0bc4402 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/dun/Rules.java @@ -5,6 +5,7 @@ import cc.winboll.studio.contacts.activities.SettingsActivity; import cc.winboll.studio.contacts.bobulltoon.TomCat; import cc.winboll.studio.contacts.model.PhoneConnectRuleBean; import cc.winboll.studio.contacts.model.SettingsBean; +import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService; import cc.winboll.studio.contacts.services.MainService; import cc.winboll.studio.contacts.utils.ContactUtils; import cc.winboll.studio.contacts.utils.IntUtils; @@ -170,6 +171,14 @@ public class Rules { isConnect = false; LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect)); } + + // 限时特殊通道打开时返回连接 + if (!isDefend && LimitedTimeSpecialChannelService.isServiceRunning()) { + LogUtils.d(TAG, String.format("PhoneNumber %s\n and Limited Time Special Channel Service Is Running.", phoneNumber)); + isDefend = true; + isConnect = true; + LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect)); + } // 检验拨不通号码群 if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) { diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/services/LimitedTimeSpecialChannelService.java b/contacts/src/main/java/cc/winboll/studio/contacts/services/LimitedTimeSpecialChannelService.java new file mode 100644 index 0000000..ae0ba19 --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/services/LimitedTimeSpecialChannelService.java @@ -0,0 +1,285 @@ +package cc.winboll.studio.contacts.services; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import cc.winboll.studio.libappbase.LogUtils; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/04/18 15:00:00 (香港时区) + * @LastEditTime 2026/04/18 22:15:00 (香港时区) + * @Describe 限时特殊通道服务 + * 提供安全的服务启动、令牌校验及循环任务管理 + * 新增功能: + * 1. 计时过程中实时输出剩余秒数 + * 2. 服务销毁前,发送本地广播通知应用内其他组件 + */ +public class LimitedTimeSpecialChannelService extends Service { + + // ========================= 常量定义 ========================= + public static final String TAG = "LimitedTimeSpecialChannelService"; + public static final String EXTRA_DELAY_MILLIS = "EXTRA_DELAY_MILLIS"; + private static final String EXTRA_SECURITY_TOKEN = "EXTRA_SECURITY_TOKEN"; + + // 新增:本地广播 Action 常量 + public static final String ACTION_SERVICE_DESTROYED = "cc.winboll.studio.contacts.services.ACTION_SERVICE_DESTROYED"; + + // ========================= 静态变量 ========================= + /** + * 公共静态私有字段:安全校验令牌 + * 确保全局可访问且值不可变更 + */ + public static final String mValidToken = "VALID_TOKEN_" + System.currentTimeMillis(); + + private static volatile LimitedTimeSpecialChannelService sInstance = null; + private static volatile boolean sIsServiceRunning = false; + + // ========================= 成员变量 ========================= + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private long mTotalMillis = 0; // 总定时时长 + private long mRemainingMillis = 0; // 剩余时长 + private LocalBroadcastManager mLocalBroadcastManager; // 本地广播管理器实例 + + // ========================= 公共静态方法 ========================= + /** + * 公共静态方法:启动服务 + * @param context 上下文 + * @param delayMillis 定时时长(毫秒) + */ + public static void startService(Context context, long delayMillis) { + LogUtils.i(TAG, "调用 startService 入口"); + LogUtils.i(TAG, "入参 - context: " + context + ", delayMillis: " + delayMillis); + + if (context == null) { + LogUtils.w(TAG, "启动失败,上下文为null"); + return; + } + if (isServiceRunning()) { + LogUtils.i(TAG, "服务已运行,忽略重复启动"); + return; + } + + // 构建Intent传递参数 + Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class); + intent.putExtra(EXTRA_SECURITY_TOKEN, mValidToken); + intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis); + + context.startService(intent); + LogUtils.i(TAG, "服务启动命令已发出"); + } + + /** + * 公共静态方法:停止服务 + * @param context 上下文 + */ + public static void stopService(Context context) { + LogUtils.i(TAG, "调用 stopService 入口"); + LogUtils.i(TAG, "入参 - context: " + context); + + if (context == null) { + LogUtils.w(TAG, "停止失败,上下文为null"); + return; + } + Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class); + context.stopService(intent); + LogUtils.i(TAG, "服务停止命令已发出"); + } + + /** + * 公共静态方法:查询服务运行状态 + * @return true if running + */ + public static boolean isServiceRunning() { + return sIsServiceRunning; + } + + /** + * 【核心单元测试方法】 + * 执行完整的单元测试流程 + * @param context 上下文 + */ + public static void unitTest(Context context) { + LogUtils.i(TAG, "=== 开始执行单元测试 ==="); + + // 测试1: 初始状态应为未运行 + boolean initialState = isServiceRunning(); + LogUtils.i(TAG, "测试1 - 初始状态检查: " + (initialState ? "失败(不应运行)" : "成功(未运行)")); + + // 启动服务,设置5秒定时 + startService(context, 5000L); + + // 核心修复:同步等待服务启动完成 + try { + Thread.sleep(1200); + } catch (InterruptedException e) { + LogUtils.e(TAG, "单元测试等待被中断", e); + } + + // 测试3: 验证服务已启动 + boolean runningState = isServiceRunning(); + LogUtils.i(TAG, "测试3 - 运行状态检查: " + (runningState ? "成功(运行中)" : "失败(未运行)")); + + // 测试4: 尝试重复启动 + LogUtils.i(TAG, "测试4 - 尝试重复启动(预期忽略)"); + startService(context, 5000L); + + // 测试5: 等待服务自动停止 + LogUtils.i(TAG, "测试5 - 等待服务自动停止(5秒)..."); + try { + // 等待超过服务的5秒运行时间,确保能观测到销毁状态 + Thread.sleep(6000); + } catch (InterruptedException e) { + LogUtils.e(TAG, "测试等待被中断", e); + } + + // 验证最终状态 + boolean finalState = isServiceRunning(); + LogUtils.i(TAG, "测试5 - 最终状态检查: " + (!finalState ? "成功(已销毁)" : "失败(仍运行)")); + LogUtils.i(TAG, "=== 单元测试执行完成 ==="); + } + + // ========================= 生命周期 ========================= + @Override + public void onCreate() { + super.onCreate(); + LogUtils.i(TAG, "服务 onCreate,创建实例"); + sInstance = this; + sIsServiceRunning = true; + + // 初始化本地广播管理器 + mLocalBroadcastManager = LocalBroadcastManager.getInstance(this); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + LogUtils.i(TAG, "服务 onStartCommand,处理启动请求"); + LogUtils.i(TAG, "入参 - intent: " + intent + ", flags: " + flags + ", startId: " + startId); + + if (!isValidToken(intent)) { + LogUtils.w(TAG, "安全校验失败,拒绝启动"); + stopSelf(); + return START_NOT_STICKY; + } + + long delayMillis = intent.getLongExtra(EXTRA_DELAY_MILLIS, 0); + if (delayMillis <= 0) { + LogUtils.w(TAG, "无效的定时时长: " + delayMillis + ",服务将退出"); + stopSelf(); + return START_NOT_STICKY; + } + + // 初始化总时长和剩余时长 + mTotalMillis = delayMillis; + mRemainingMillis = delayMillis; + LogUtils.i(TAG, "初始化定时时长: " + (mTotalMillis / 1000) + " 秒"); + + startLoopTask(); + // 设置自动停止定时器 + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + LogUtils.i(TAG, "定时结束,准备停止服务"); + stopSelf(); + } + }, delayMillis); + + return START_STICKY; + } + + @Override + public void onDestroy() { + LogUtils.i(TAG, "服务 onDestroy,准备销毁"); + + // 新增:发送本地广播,通知应用内其他组件服务即将销毁 + sendServiceDestroyedBroadcast(); + + // 原有逻辑:重置状态 + sIsServiceRunning = false; + sInstance = null; + + // 原有逻辑:清理所有回调 + mHandler.removeCallbacksAndMessages(null); + + // 原有逻辑:执行父类销毁 + super.onDestroy(); + + LogUtils.i(TAG, "服务销毁完成"); + } + + /** + * 新增:发送服务销毁的本地广播 + * 确保应用内所有注册了该广播接收器的组件都能收到通知 + */ + private void sendServiceDestroyedBroadcast() { + try { + Intent intent = new Intent(ACTION_SERVICE_DESTROYED); + // 可在此处添加额外数据,例如剩余时长等 + intent.putExtra("REMAINING_SECONDS", mRemainingMillis / 1000); + // 使用本地广播管理器发送 + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + LogUtils.i(TAG, "已发送本地广播 - 服务销毁通知: " + ACTION_SERVICE_DESTROYED); + } catch (Exception e) { + LogUtils.e(TAG, "发送服务销毁广播失败", e); + } + } + + @Override + public IBinder onBind(Intent intent) { + LogUtils.i(TAG, "服务 onBind,不支持绑定"); + // 不支持绑定 + return null; + } + + // ========================= 私有辅助方法 ========================= + /** + * 校验令牌有效性 + */ + private boolean isValidToken(Intent intent) { + LogUtils.i(TAG, "调用 isValidToken 校验"); + LogUtils.i(TAG, "入参 - intent: " + intent); + + if (intent == null) { + return false; + } + String incomingToken = intent.getStringExtra(EXTRA_SECURITY_TOKEN); + LogUtils.i(TAG, "接收到外部传入令牌: " + incomingToken); + LogUtils.i(TAG, "比对本地存储令牌: " + mValidToken); + return mValidToken.equals(incomingToken); + } + + /** + * 启动循环任务 + */ + private void startLoopTask() { + mHandler.removeCallbacks(mLoopTaskRunnable); + mHandler.postDelayed(mLoopTaskRunnable, 1000); + } + + /** + * 循环任务心跳 + * 新增:实时计算并输出剩余秒数 + */ + private final Runnable mLoopTaskRunnable = new Runnable() { + @Override + public void run() { + if (sIsServiceRunning) { + // 计算剩余秒数 + long remainingSeconds = mRemainingMillis / 1000; + // 输出剩余秒数信息 + LogUtils.i(TAG, "循环任务心跳:服务运行中,剩余 " + remainingSeconds + " 秒"); + + // 剩余时长递减 + mRemainingMillis -= 1000; + + // 递归调用,形成循环 + startLoopTask(); + } + } + }; +} + diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/views/LimitedTimeSpecialChannelView.java b/contacts/src/main/java/cc/winboll/studio/contacts/views/LimitedTimeSpecialChannelView.java new file mode 100644 index 0000000..918068e --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/views/LimitedTimeSpecialChannelView.java @@ -0,0 +1,252 @@ +package cc.winboll.studio.contacts.views; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Switch; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import cc.winboll.studio.contacts.R; +import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; + +/** + * @Author ZhanGSKen + * @Date 2026/04/18 15:05 + * @LastEditTime 2026/04/18 22:30:00 (香港时区) + * @Describe 限时特殊通道视图 + * 功能更新: + * 1. Switch 打开时,EditText 不可编辑,布局背景变为绿色 + * 2. Switch 关闭时,EditText 可编辑,布局背景恢复为白色 + * 3. 监听服务销毁本地广播,到达后自动关闭 Switch 并恢复背景色 + */ +public class LimitedTimeSpecialChannelView extends LinearLayout { + + // ====================== 常量定义 ========================= + public static final String TAG = "LimitedTimeSpecialChannelView"; + + // ====================== 成员变量 ========================= + private Context mContext; + private EditText mEtSeconds; + private Switch mSwEnable; + private LinearLayout mLlMain; + + // ====================== 构造函数 ========================= + /** + * 代码创建View时调用 + * @param context 上下文 + */ + public LimitedTimeSpecialChannelView(Context context) { + super(context); + initView(context); + } + + /** + * XML布局中引用时调用 + * @param context 上下文 + * @param attrs 属性集合 + */ + public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + /** + * 指定样式属性 + * @param context 上下文 + * @param attrs 属性集合 + * @param defStyleAttr 默认样式属性 + */ + public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context); + } + + /** + * 指定样式资源 + * @param context 上下文 + * @param attrs 属性集合 + * @param defStyleAttr 默认样式属性 + * @param defStyleRes 默认样式资源 + */ + public LimitedTimeSpecialChannelView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initView(context); + } + + // ====================== 初始化方法 ======================== + /** + * 初始化视图 + * 加载布局文件,绑定控件,并注册广播接收器 + * @param context 上下文 + */ + private void initView(Context context) { + LogUtils.i(TAG, "初始化限时特殊通道视图"); + this.mContext = context; + + // 1. 加载布局文件 + LayoutInflater.from(context) + .inflate(R.layout.view_limitedtimespecialchannel, this, true); + + LogUtils.i(TAG, "布局加载完成:view_limitedtimespecialchannel.xml"); + + // 2. 绑定核心控件引用 + bindViews(); + + // 3. 初始化开关状态与背景色关联 + initSwitchState(); + + // 4. 注册服务销毁本地广播监听 + registerServiceDestroyedListener(); + } + + /** + * 绑定布局中的控件ID + */ + private void bindViews() { + mEtSeconds = (EditText) findViewById(R.id.et_seconds); + mSwEnable = (Switch) findViewById(R.id.sw_enable); + mLlMain = (LinearLayout) findViewById(R.id.ll_main); + + // 健壮性检查 + if (mEtSeconds == null) { + LogUtils.e(TAG, "未找到ID为 et_seconds 的EditText"); + } + if (mSwEnable == null) { + LogUtils.e(TAG, "未找到ID为 sw_enable 的Switch"); + } + if (mLlMain == null) { + LogUtils.w(TAG, "未找到ID为 ll_main 的LinearLayout,将无法设置背景色"); + } + } + + /** + * 初始化开关状态 + * 设置Switch的监听事件,以及初始背景色 + */ + private void initSwitchState() { + // 设置初始状态 + boolean isChecked = mSwEnable.isChecked(); + mEtSeconds.setEnabled(!isChecked); + if (mLlMain != null) { + mLlMain.setBackgroundColor(isChecked ? android.graphics.Color.GREEN : android.graphics.Color.WHITE); + } + LogUtils.i(TAG, "初始状态 - 开关: " + (isChecked ? "启用" : "关闭") + ", 背景色: " + (isChecked ? "绿色" : "白色")); + + // 为Switch设置切换监听 + mSwEnable.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + LogUtils.i(TAG, "开关状态变更: " + (isChecked ? "启用" : "关闭")); + + // 核心逻辑:控制EditText的可编辑状态 + mEtSeconds.setEnabled(!isChecked); + + // 控制布局背景色 + if (mLlMain != null) { + mLlMain.setBackgroundColor(isChecked ? android.graphics.Color.GREEN : android.graphics.Color.WHITE); + } + + // 控制服务启动与停止 + if (isChecked) { + // 打开开关时,启动服务 + startLimitedTimeSpecialChannelService(); + } else { + // 关闭开关时,停止服务 + LimitedTimeSpecialChannelService.stopService(mContext); + } + } + }); + } + + /** + * 注册服务销毁本地广播监听 + * 当服务销毁消息到达时,自动关闭开关并恢复背景色 + */ + private void registerServiceDestroyedListener() { + // 创建广播接收器 + LocalBroadcastManager.getInstance(mContext) + .registerReceiver(new android.content.BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // 校验广播Action + if (LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED.equals(intent.getAction())) { + LogUtils.i(TAG, "收到服务销毁本地广播,准备关闭开关并恢复背景"); + + // 核心逻辑:关闭Switch + if (mSwEnable != null && mSwEnable.isChecked()) { + mSwEnable.setChecked(false); + } + + // 恢复布局背景色为白色 + if (mLlMain != null) { + mLlMain.setBackgroundColor(android.graphics.Color.WHITE); + } + + // 可选:通知用户服务已销毁 + ToastUtils.show("服务已销毁,恢复初始状态"); + } + } + }, new IntentFilter(LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED)); + } + + /** + * 启动限时特殊通道服务 + * 读取输入框中的秒数,转换为毫秒后启动服务 + */ + private void startLimitedTimeSpecialChannelService() { + LogUtils.i(TAG, "检测到秒数输入框点击,准备启动服务"); + + // 1. 获取输入的秒数 + String secondsStr = mEtSeconds.getText().toString().trim(); + if (secondsStr.isEmpty()) { + LogUtils.w(TAG, "输入框为空,使用默认值 3600 秒"); + secondsStr = "3600"; + } + + try { + // 2. 转换为毫秒数 (1秒 = 1000毫秒) + long seconds = Long.parseLong(secondsStr); + long delayMillis = seconds * 1000; + + LogUtils.i(TAG, "输入秒数: " + seconds + " 秒,转换为毫秒: " + delayMillis); + + // 3. 调用服务启动方法 + ToastUtils.show("调用 LimitedTimeSpecialChannelService 服务"); + LimitedTimeSpecialChannelService.startService(mContext, delayMillis); + + } catch (NumberFormatException e) { + LogUtils.e(TAG, "输入的秒数格式错误: " + secondsStr, e); + } + } + + /** + *【可选】视图销毁时反注册广播,防止内存泄漏 + */ +// @Override +// protected void onDetachedFromWindow() { +// super.onDetachedFromWindow(); +// // 反注册本地广播 +// LocalBroadcastManager.getInstance(mContext).unregisterregisterReceiver(mServiceDestroyedReceiver); +// LogUtils.i(TAG, "反注册服务销毁本地广播完成"); +// } + +// //【可选】定义广播接收器成员变量,方便反注册 +// private android.content.BroadcastReceiver mServiceDestroyedReceiver; +// +// private void registerServiceDestroyedListener() { +// mServiceDestroyedReceiver = new android.content.BroadcastReceiver() { +// @Override +// public void onReceive(Context context, Intent intent) { +// // ... 原有onReceive逻辑 ... +// } +// }; +// LocalBroadcastManager.getInstance(mContext).registerReceiver(mServiceDestroyedReceiver, new IntentFilter(LimitedTimeSpecialChannelService.ACTION_SERVICE_DESTROYED)); +// } +} + diff --git a/contacts/src/main/res/layout/activity_settings.xml b/contacts/src/main/res/layout/activity_settings.xml index 6ab5717..08335f9 100644 --- a/contacts/src/main/res/layout/activity_settings.xml +++ b/contacts/src/main/res/layout/activity_settings.xml @@ -57,6 +57,24 @@ android:id="@+id/tv_DunInfo"/> + + + + + + + + + + + + + + + + + + +