feat: 增强配置刷新机制并优化时间管控

增加跨天配置自动重试、推送防抖节流与定时轮询兜底,修复连接节流失效问题;
优化锁屏状态检测与应用强杀逻辑,升级版本号至 3.5.3。
This commit is contained in:
2026-07-27 09:32:23 +08:00
parent e5c4829e7e
commit a7c6e1a00a
7 changed files with 266 additions and 11 deletions

View File

@@ -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
}
}

View File

@@ -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 重启后是否连接

View File

@@ -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<String, Long> 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<SnTimeControlInfo> 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<List<AppTimeControlInfo>> response) {
mGetAppTimeControlInfoSuccessful = true;
Log.e(TAG, "getAppTimeControlInfo onNext: " + response);
if (response.code == 200 && response.data != null) {
List<AppTimeControlInfo> 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 ---

View File

@@ -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

View File

@@ -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;
/**
* 推送触发刷新的短节流 key1 分钟内因推送触发的多次刷新只真正请求一次)。
*/
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();
//避免同时请求

View File

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

View File

@@ -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";