feat: 增强配置刷新机制并优化时间管控
增加跨天配置自动重试、推送防抖节流与定时轮询兜底,修复连接节流失效问题; 优化锁屏状态检测与应用强杀逻辑,升级版本号至 3.5.3。
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 重启后是否连接
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
//避免同时请求
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user