From a7c6e1a00a34e6ad53154310153ab952b49b1fa5 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Mon, 27 Jul 2026 09:32:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E6=9C=BA=E5=88=B6=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E7=AE=A1=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加跨天配置自动重试、推送防抖节流与定时轮询兜底,修复连接节流失效问题; 优化锁屏状态检测与应用强杀逻辑,升级版本号至 3.5.3。 --- app/build.gradle | 13 +- .../com/fuying/sn/manager/ConnectManager.java | 8 + .../fuying/sn/manager/time/TimeManager.java | 138 +++++++++++++++++- .../sn/network/NetInterfaceManager.java | 2 + .../java/com/fuying/sn/push/PushManager.java | 108 +++++++++++++- .../com/fuying/sn/service/ManagerService.java | 7 + .../java/com/fuying/sn/utils/JgyUtils.java | 1 + 7 files changed, 266 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4769ee9..c688e60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,8 +69,8 @@ android { official { flavorDimensions "default" - versionCode 92 - versionName "3.5.1" + versionCode 94 + versionName "3.5.3" applicationId "com.fuying.sn" buildConfigField "String", "ROOT_URL", '"https://as.fuyingy.com/android/"' @@ -479,15 +479,12 @@ android { applicationVariants.all { variant -> variant.outputs.each { output -> def buildType = variant.buildType.name - def fileName = "" - if (buildType.contains("debug")) { - fileName = "${appName()}_V${defaultConfig.versionName}_${releaseTime()}.apk" +// fileName = "${appName()}_V${defaultConfig.versionName}_${releaseTime()}.apk" + output.outputFileName = "app_debug.apk" } else { - fileName = "${appName()}_${variant.versionCode}_V${variant.versionName}_${releaseTime()}_${buildType}.apk" + output.outputFileName = "${appName()}_${variant.versionCode}_V${variant.versionName}_${releaseTime()}_${buildType}.apk" } - - output.outputFileName = fileName } } diff --git a/app/src/main/java/com/fuying/sn/manager/ConnectManager.java b/app/src/main/java/com/fuying/sn/manager/ConnectManager.java index 2f667d2..80ae011 100644 --- a/app/src/main/java/com/fuying/sn/manager/ConnectManager.java +++ b/app/src/main/java/com/fuying/sn/manager/ConnectManager.java @@ -95,6 +95,14 @@ public class ConnectManager { return nowTime - lastTime > intervalTime && nowTime - lastTime > ONE_SECOND_TIME; } + /** + * 请求成功后记录本次连接时间,供 {@link #isNeedConnect(String, ConnectMode)} 做节流判断。 + * 注意:原实现从未写入该时间戳,导致节流机制形同虚设,这里补齐。 + */ + public void updateConnectTime(String key) { + mMMKV.encode(key, System.currentTimeMillis()); + } + /** * @return 重启后是否连接 diff --git a/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java b/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java index aeb9e65..274104d 100644 --- a/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java +++ b/app/src/main/java/com/fuying/sn/manager/time/TimeManager.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.IActivityManager; +import android.app.KeyguardManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -32,6 +33,7 @@ import com.google.gson.reflect.TypeToken; import com.hjq.toast.Toaster; import com.tencent.mmkv.MMKV; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.time.DayOfWeek; import java.time.LocalDate; @@ -75,6 +77,8 @@ public class TimeManager { private SnTimeControlInfo mSnTimeControlInfo; private boolean mGetSnTimeControlInfoSuccessful = false; + private boolean mGetAppTimeControlInfoSuccessful = false; + /** * 保存所有应用的管控配置(Key 为包名) @@ -91,6 +95,19 @@ public class TimeManager { */ private Map mRemainingTimeMap = new HashMap<>(); + /** + * 记录上一次已处理过的日期,用于检测跨天(0点)后刷新配置 + */ + private LocalDate mLastProcessedDate; + + /** + * 跨天刷新配置时的重试定时器,网络异常时用于周期性重试直至成功 + */ + private Disposable mConfigRetryDisposable; + private static final long CONFIG_RETRY_INTERVAL = 60; // 重试间隔(秒) + private static final int MAX_CONFIG_RETRY_COUNT = 10; // 跨天刷新配置的最大重试次数,超过后停止重试 + private int mConfigRetryCount = 0; // 已执行重试的次数 + /** * 免管控白名单,包含系统核心组件及特定工具应用 */ @@ -127,6 +144,7 @@ public class TimeManager { initActivityController(); initUidImportanceListener(); loadLocalConfig(); + mLastProcessedDate = LocalDate.now(); getTimeControl(); // 异步同步服务端配置 startInIntervalTask(); // 启动每秒检测任务 @@ -249,19 +267,35 @@ public class TimeManager { */ private void checkForegroundAppName() { String pkg = getTopActivityPackageName(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "checkForegroundAppName: " + pkg); + } if (TextUtils.isEmpty(pkg)) return; + //如果应用没有退出锁屏,获取到的还是应用的包名 + if (isDeviceLocked(mContext)) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "checkForegroundAppName: DeviceLocked "); + } +// if (!allowPackage.contains(pkg)) { +// killBackgroundProcesses(pkg); +// } + return; + } + // 1. 豁免条件:白名单或家长管控总开关已关闭 if (isPackageExempted(pkg)) return; // 2. 检查基本配置是否就绪 if (mSnTimeControlInfo == null) { if (!mGetSnTimeControlInfoSuccessful) getSnTimeControl(); + if (!mGetAppTimeControlInfoSuccessful) getAppTimeControlInfo(); return; } // 3. 运行权限动态校验 if (!appCanRun(pkg)) { + killBackgroundProcesses(pkg); gotoLauncher(); return; } @@ -382,6 +416,18 @@ public class TimeManager { } } + /** + * 判断当前是否处于锁屏状态 + */ + public boolean isDeviceLocked(Context context) { + KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + if (km != null) { + // isKeyguardLocked() 返回 true 表示当前屏幕是锁定的(无论是有安全锁还是滑动锁) + return km.isKeyguardLocked(); + } + return false; + } + /** * 获取最顶层的应用包名 */ @@ -514,6 +560,7 @@ public class TimeManager { @Override public void onNext(@NonNull BaseResponse response) { + Log.e(TAG, "getSnTimeControlObservable onNext: " + response); mGetSnTimeControlInfoSuccessful = true; if (response.code == 200 && response.data != null) { mSnTimeControlInfo = response.data; @@ -525,6 +572,7 @@ public class TimeManager { @Override public void onError(@NonNull Throwable e) { + mGetSnTimeControlInfoSuccessful = false; Log.e(TAG, "getSnTimeControl failed: " + e.getMessage()); } @@ -543,6 +591,7 @@ public class TimeManager { @Override public void onNext(@NonNull BaseResponse> response) { + mGetAppTimeControlInfoSuccessful = true; Log.e(TAG, "getAppTimeControlInfo onNext: " + response); if (response.code == 200 && response.data != null) { List list = response.data; @@ -553,6 +602,7 @@ public class TimeManager { @Override public void onError(@NonNull Throwable e) { + mGetAppTimeControlInfoSuccessful = false; Log.e(TAG, "getAppTimeControlInfo failed: " + e.getMessage()); } @@ -573,15 +623,99 @@ public class TimeManager { mContext.registerReceiver(new TimeChangedReceiver(), filter); } - private static class TimeChangedReceiver extends BroadcastReceiver { + private class TimeChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "System time/date changed: " + intent.getAction()); + handleDateChanged(); } } + /** + * 检测是否跨天(0点)。跨天后先用本地缓存配额乐观重置当日剩余时长, + * 避免旧值(可能已耗尽)继续拦截设备;随后重新从服务端拉取最新配置。 + */ + private void handleDateChanged() { + LocalDate today = LocalDate.now(); + Log.d(TAG, "handleDateChanged: today = " + today); + Log.d(TAG, "handleDateChanged: mLastProcessedDate = " + mLastProcessedDate); + if (today.equals(mLastProcessedDate)) { + // 日期未变化(如仅时间微调、每分钟的 TIME_TICK): + // 若两级配置尚未成功拉取(如启动时拉取失败),则尝试重新请求以恢复管控配置。 + // 仅在当前无重试任务在跑时触发,避免重复创建定时器。 + if ((!mGetSnTimeControlInfoSuccessful || !mGetAppTimeControlInfoSuccessful) + && (mConfigRetryDisposable == null || mConfigRetryDisposable.isDisposed())) { + Log.d(TAG, "Date unchanged but config not fully fetched, trying to fetch config."); + fetchConfigWithRetry(); + } + return; + } + mLastProcessedDate = today; + Log.d(TAG, "Date changed to " + today + ", resetting daily quota and fetching latest config."); + + // 日期变化:先将两级配置成功标记置为 false,强制跨天后重新校验与拉取 + mGetSnTimeControlInfoSuccessful = false; + mGetAppTimeControlInfoSuccessful = false; + + // 乐观重置:使用本地缓存的当日配额重置剩余时长,保证跨天后立即可用 + if (mSnTimeControlInfo != null) { + mGlobalRemainingTime = mSnTimeControlInfo.getToday_time(); + mMMKV.encode(GLOBAL_REMAINING_TIME_KEY, mGlobalRemainingTime); + } + if (mAppTimeControlMap != null && !mAppTimeControlMap.isEmpty()) { + mRemainingTimeMap = mAppTimeControlMap.values().stream() + .collect(Collectors.toMap(AppTimeControlInfo::getApp_package, + AppTimeControlInfo::getToday_time, (v1, v2) -> v1)); + } + + // 重新拉取服务端最新配置(含当日最新配额与时间管控规则), + // 若因网络问题获取失败则自动重试,直至两级配置均成功获取 + mConfigRetryCount = 0; + fetchConfigWithRetry(); + } + + /** + * 跨天刷新配置:先发起一次请求,若 SN 或应用任一级配置未成功获取(网络异常等), + * 则按固定间隔重试,直到两级均成功,确保设备最终能拿到最新配置。 + */ + private void fetchConfigWithRetry() { + getTimeControl(); + if (mConfigRetryDisposable != null && !mConfigRetryDisposable.isDisposed()) { + mConfigRetryDisposable.dispose(); + } + // 已达最大重试次数则停止重试,避免无限占用资源 + if (mConfigRetryCount >= MAX_CONFIG_RETRY_COUNT) { + Log.w(TAG, "Config fetch reached max retry count (" + MAX_CONFIG_RETRY_COUNT + "), stop retrying."); + return; + } + mConfigRetryCount++; + mConfigRetryDisposable = Observable.timer(CONFIG_RETRY_INTERVAL, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aLong -> { + if (mGetSnTimeControlInfoSuccessful && mGetAppTimeControlInfoSuccessful) { + Log.d(TAG, "Latest config fetched successfully after retry."); + mConfigRetryDisposable = null; + mConfigRetryCount = 0; + return; + } + Log.d(TAG, "Config fetch incomplete (sn=" + mGetSnTimeControlInfoSuccessful + + ", app=" + mGetAppTimeControlInfoSuccessful + "), retrying... (" + + mConfigRetryCount + "/" + MAX_CONFIG_RETRY_COUNT + ")"); + fetchConfigWithRetry(); + }, throwable -> Log.e(TAG, "Config retry timer error: " + throwable.getMessage())); + } + + public void killBackgroundProcesses(String processName) { + ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + activityManager.forceStopPackage(processName); + } + public void onDestroy() { - // 必要时清理资源 + if (mConfigRetryDisposable != null && !mConfigRetryDisposable.isDisposed()) { + mConfigRetryDisposable.dispose(); + mConfigRetryDisposable = null; + } } // --- Getters & Setters --- diff --git a/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java b/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java index 31a9d0d..46a40d7 100644 --- a/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java +++ b/app/src/main/java/com/fuying/sn/network/NetInterfaceManager.java @@ -2582,6 +2582,7 @@ public class NetInterfaceManager { }); } + @Deprecated public void sendCloseApp(String packageName, CompleteCallback completeCallback) { int isLogined = (int) SPUtils.get(mContext, CommonConfig.isLogined, 2); if (isLogined != 1) { @@ -3410,6 +3411,7 @@ public class NetInterfaceManager { } else { } + } @Override diff --git a/app/src/main/java/com/fuying/sn/push/PushManager.java b/app/src/main/java/com/fuying/sn/push/PushManager.java index 2bff139..0856af7 100644 --- a/app/src/main/java/com/fuying/sn/push/PushManager.java +++ b/app/src/main/java/com/fuying/sn/push/PushManager.java @@ -28,9 +28,12 @@ import com.fuying.sn.dialog.CustomDialog; import com.fuying.sn.disklrucache.CacheHelper; import com.fuying.sn.gson.GsonUtils; import com.fuying.sn.manager.ControlManager; +import com.fuying.sn.manager.ConnectManager; +import com.fuying.sn.manager.ConnectMode; import com.fuying.sn.manager.DeviceManager; import com.fuying.sn.manager.time.TimeManager; import com.fuying.sn.network.NetInterfaceManager; +import com.fuying.sn.network.UrlAddress; import com.fuying.sn.receiver.BootReceiver; import com.fuying.sn.service.LogcatService; import com.fuying.sn.service.ManagerService; @@ -94,10 +97,31 @@ public class PushManager { this.mCacheHelper = new CacheHelper(context); } + /** + * 定时轮询兜底间隔:推送不可靠时,靠它保证最终能拿到最新配置。 + * 15 分钟一次,配合下方短节流,不会频繁请求接口。 + */ + private static final long POLLING_INTERVAL = ConnectManager.FIFTEEN_MINUTES_TIME; + /** + * 推送防抖窗口:短时间内连续多条推送,合并为一次刷新。 + */ + private static final long DEBOUNCE_WINDOW = 3 * 1000L; + /** + * 推送触发刷新的短节流 key(1 分钟内因推送触发的多次刷新只真正请求一次)。 + */ + private static final String KEY_PUSH_REFRESH = "push_config_refresh"; + + private Handler mSyncHandler; + private Runnable mPollRunnable; + private Runnable mDebounceRunnable; + private boolean mPollingStarted = false; + public static void init(Context context) { if (sInstance == null) { sInstance = new PushManager(context); } + // 启动定时轮询兜底(推送可能收不到,轮询保证最终一致) + sInstance.startPolling(); } public static PushManager getInstance() { @@ -566,6 +590,8 @@ public class PushManager { break; default: } + // 无论收到何种推送,都触发一次配置刷新(防抖+短节流,保证最新且不过频) + scheduleRefresh(); } @@ -580,11 +606,91 @@ public class PushManager { NetInterfaceManager.getInstance().getMachineTimeControl(); NetInterfaceManager.getInstance().getSnTimeControl(); TimeManager.getInstance().getTimeControl(); - + // 记录本次刷新时间,供短节流判断 + ConnectManager.getInstance().updateConnectTime(KEY_PUSH_REFRESH); } }, 2345); } + /** + * 启动定时轮询兜底。进程存活期间,每隔 {@link #POLLING_INTERVAL} 主动拉取一次关键配置, + * 解决"推送收不到导致配置永远不更新"的问题。轮询间隔(15分钟)本身即天然限频。 + */ + public void startPolling() { + if (mPollingStarted) { + return; + } + mPollingStarted = true; + if (mSyncHandler == null) { + mSyncHandler = new Handler(Looper.getMainLooper()); + } + mPollRunnable = new Runnable() { + @Override + public void run() { + Log.e(TAG, "polling tick -> updateUserSettings"); + updateUserSettings(); + if (mSyncHandler != null) { + mSyncHandler.postDelayed(this, POLLING_INTERVAL); + } + } + }; + // 首次轮询延迟一个间隔再执行,避免与启动/网络恢复时的即时刷新叠加 + mSyncHandler.postDelayed(mPollRunnable, POLLING_INTERVAL); + } + + public void stopPolling() { + mPollingStarted = false; + if (mSyncHandler != null && mPollRunnable != null) { + mSyncHandler.removeCallbacks(mPollRunnable); + } + } + + /** + * 推送触发:防抖合并 + 短节流。 + * 多条推送在 {@link #DEBOUNCE_WINDOW} 内到达只合并为一次刷新; + * 且 1 分钟内的多次推送触发只真正请求一次,避免推送风暴导致频繁请求接口。 + */ + public void scheduleRefresh() { + if (mSyncHandler == null) { + mSyncHandler = new Handler(Looper.getMainLooper()); + } + if (mDebounceRunnable == null) { + mDebounceRunnable = new Runnable() { + @Override + public void run() { + if (ConnectManager.getInstance().isNeedConnect(KEY_PUSH_REFRESH, ConnectMode.ONE_MINUTE)) { + Log.e(TAG, "scheduleRefresh -> updateUserSettings"); + updateUserSettings(); + } else { + Log.e(TAG, "scheduleRefresh skipped (throttled)"); + } + } + }; + } + mSyncHandler.removeCallbacks(mDebounceRunnable); + mSyncHandler.postDelayed(mDebounceRunnable, DEBOUNCE_WINDOW); + } + + /** + * 立即刷新(绕过防抖,但仍受短节流保护),用于网络恢复、开机完成等场景。 + */ + public void triggerRefresh() { + if (mSyncHandler == null) { + mSyncHandler = new Handler(Looper.getMainLooper()); + } + mSyncHandler.post(new Runnable() { + @Override + public void run() { + if (ConnectManager.getInstance().isNeedConnect(KEY_PUSH_REFRESH, ConnectMode.ONE_MINUTE)) { + Log.e(TAG, "triggerRefresh -> updateUserSettings"); + updateUserSettings(); + } else { + Log.e(TAG, "triggerRefresh skipped (throttled)"); + } + } + }); + } + private void addApkToWhiteList(String extras) { Random random = new Random(); //避免同时请求 diff --git a/app/src/main/java/com/fuying/sn/service/ManagerService.java b/app/src/main/java/com/fuying/sn/service/ManagerService.java index e6455f6..2cae013 100644 --- a/app/src/main/java/com/fuying/sn/service/ManagerService.java +++ b/app/src/main/java/com/fuying/sn/service/ManagerService.java @@ -28,6 +28,7 @@ import com.fuying.sn.config.CommonConfig; import com.fuying.sn.gson.GsonUtils; import com.fuying.sn.network.NetInterfaceManager; import com.fuying.sn.network.UrlAddress; +import com.fuying.sn.push.PushManager; import com.fuying.sn.receiver.APKinstallReceiver; import com.fuying.sn.receiver.BootReceiver; import com.fuying.sn.utils.SPUtils; @@ -74,6 +75,12 @@ public class ManagerService extends Service implements NetworkUtils.OnNetworkSta @Override public void onConnected(NetworkUtils.NetworkType networkType) { // getScreenLockState(); + // 网络恢复后立即兜底刷新一次配置(推送可能在此期间丢失) + try { + PushManager.getInstance().triggerRefresh(); + } catch (Exception e) { + Log.e(TAG, "onConnected triggerRefresh: " + e.getMessage()); + } } diff --git a/app/src/main/java/com/fuying/sn/utils/JgyUtils.java b/app/src/main/java/com/fuying/sn/utils/JgyUtils.java index dfe6231..5a6a600 100644 --- a/app/src/main/java/com/fuying/sn/utils/JgyUtils.java +++ b/app/src/main/java/com/fuying/sn/utils/JgyUtils.java @@ -160,6 +160,7 @@ public class JgyUtils { this.add(xiyouread); this.add("com.fuying.middle.english"); this.add("com.qijuqiyi"); + this.add("com.fuyingedu.fylxy_test"); }}; public static final String fxyywgj = "com.fuying.chinese";