fix: 修复重复请求拦截并优化定时关机
- 重复请求拦截器改为请求前去重并复用真实响应,避免服务端收到重复请求 - 新增 PowerOffAlarmUtils 通过 AlarmManager 精确调度定时关机 - 设备重启、服务启动、系统时间变更后重新安排关机闹钟 - 重写定时关机时间计算逻辑,修复循环关机跨天与时间格式问题 - 黑名单/白名单缓存过期时间改用默认值 - 运行应用列表排除 DX11、MX11 系统应用 - 版本号升级至 1.6.0915
This commit is contained in:
@@ -98,6 +98,7 @@ import com.aoleyun.sn.utils.ForegroundAppUtil;
|
||||
import com.aoleyun.sn.utils.JgyUtils;
|
||||
import com.aoleyun.sn.utils.MD5Util;
|
||||
import com.aoleyun.sn.utils.NetworkUtils;
|
||||
import com.aoleyun.sn.utils.PowerOffAlarmUtils;
|
||||
import com.aoleyun.sn.utils.SPUtils;
|
||||
import com.aoleyun.sn.utils.SysSettingUtils;
|
||||
import com.aoleyun.sn.utils.TimeUtils;
|
||||
@@ -759,14 +760,12 @@ public class NetInterfaceManager {
|
||||
|
||||
public Observable<BaseResponse<BlacklistBean>> getBlackListObservable() {
|
||||
return withCache(schedule(getSystemSettingApi().getBlackList(Utils.getSerial(mContext))),
|
||||
UrlAddress.BLACK_LIST,
|
||||
360 * CACHE_EXPIRE_ONE_MINUTE);
|
||||
UrlAddress.BLACK_LIST, DEFAULT_CACHE_EXPIRE);
|
||||
}
|
||||
|
||||
public Observable<BaseResponse<WhitelistBean>> getWhiteListObservable() {
|
||||
return withCache(schedule(getSystemSettingApi().getWhiteList(Utils.getSerial(mContext))),
|
||||
UrlAddress.WHITE_LIST,
|
||||
360 * CACHE_EXPIRE_ONE_MINUTE);
|
||||
UrlAddress.WHITE_LIST, DEFAULT_CACHE_EXPIRE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3610,13 +3609,13 @@ public class NetInterfaceManager {
|
||||
@Override
|
||||
public void onNext(BaseResponse<List<PoweroffBean>> listBaseResponse) {
|
||||
Log.e("getPoweroffTime", "onNext: " + listBaseResponse);
|
||||
if (listBaseResponse.isSuccess()) {
|
||||
List<PoweroffBean> poweroffBeanList = listBaseResponse.data;
|
||||
String poweroffTime = GsonUtils.toJSONString(poweroffBeanList);
|
||||
mMMKV.encode("poweroffTime", poweroffTime);
|
||||
if (listBaseResponse.isSuccess() && null != listBaseResponse.data) {
|
||||
PowerOffAlarmUtils.savePowerOffTime(GsonUtils.toJSONString(listBaseResponse.data));
|
||||
} else {
|
||||
mMMKV.encode("PoweroffTime", "");
|
||||
PowerOffAlarmUtils.savePowerOffTime("");
|
||||
}
|
||||
//关机策略更新后重新安排定时关机闹钟
|
||||
PowerOffAlarmUtils.schedule(mContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,9 +8,13 @@ import com.aoleyun.sn.utils.MD5Util;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
@@ -19,93 +23,308 @@ import okhttp3.ResponseBody;
|
||||
import okio.Buffer;
|
||||
|
||||
/**
|
||||
* v1.0 2022-07-15 16:16:52
|
||||
* 重复请求拦截器。
|
||||
*
|
||||
* <p>原实现是在 {@code chain.proceed()} 之后才判断是否为重复请求,此时请求早已发到服务器,
|
||||
* 只能丢弃返回结果,所以服务端依然会收到两次(甚至多次)请求;并且记录在 finally 中立即删除,
|
||||
* 去重窗口几乎为零,重复请求根本拦不住。</p>
|
||||
*
|
||||
* <p>优化后的策略:</p>
|
||||
* <ul>
|
||||
* <li>发请求之前先判断是否已经有相同请求:有则等待它的结果并复用,不再访问服务器;</li>
|
||||
* <li>请求完成后在 {@link #REUSE_WINDOW_MS} 内保留结果,短时间内相同的请求直接复用结果;</li>
|
||||
* <li>复用时使用真实的状态码、响应头和响应体,调用方无感知(原来的空 {@link Response} 会让
|
||||
* 解析层直接抛异常);</li>
|
||||
* <li>无响应体或响应体过大(超过 {@link #MAX_DEDUP_BODY_BYTES})的请求不参与去重,直接透传,
|
||||
* 避免占用大量内存。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>v1.0 2022-07-15 16:16:52</p>
|
||||
*/
|
||||
public class RepeatRequestInterceptor implements Interceptor {
|
||||
private static final String TAG = "RepeatRequestInterceptor";
|
||||
|
||||
private final ConcurrentHashMap<String, Long> requestIdsMap = new ConcurrentHashMap<>();
|
||||
public static final String REPEAT_REQUEST_PROTOCOL = "OKHTTP_REPEAT_REQUEST_PROTOCOL";
|
||||
/**
|
||||
* 等待进行中请求返回结果的最长时间,超时后本次请求自己访问服务器。
|
||||
* 需要大于 OkHttpClient 的超时时间(当前为 5 秒)。
|
||||
*/
|
||||
private static final long WAIT_IN_FLIGHT_TIMEOUT_MS = 10_000L;
|
||||
|
||||
/**
|
||||
* 请求完成后结果保留的时间。在该时间内收到相同请求,直接复用结果,不再访问服务器。
|
||||
* 需要小于业务侧的刷新间隔(ConnectMode 最小为 1 分钟)。
|
||||
*/
|
||||
private static final long REUSE_WINDOW_MS = 500L;
|
||||
|
||||
/**
|
||||
* 超过该大小的响应体不参与去重(直接透传),避免内存占用
|
||||
*/
|
||||
private static final long MAX_DEDUP_BODY_BYTES = 1024 * 1024L;
|
||||
|
||||
/**
|
||||
* 参与去重 key 计算的请求体最大字节数
|
||||
*/
|
||||
private static final int MAX_BODY_BYTES_FOR_KEY = 8 * 1024;
|
||||
|
||||
/**
|
||||
* 日志中请求体、响应体的最大长度
|
||||
*/
|
||||
private static final int MAX_LOG_BODY_LENGTH = 10240;
|
||||
|
||||
private final ConcurrentHashMap<String, RepeatRecord> recordMap = new ConcurrentHashMap<>();
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Response intercept(@NotNull Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
Response response = chain.proceed(request);
|
||||
ResponseBody responseBody = response.body();
|
||||
String bodyText = requestBodyToString(request.body());
|
||||
String requestKey = buildRequestKey(request, bodyText);
|
||||
|
||||
//responseBody.string()消费了请求,就会导致多次请求。
|
||||
String content = responseBody.string();
|
||||
// Response copy = response.newBuilder().body(responseBody).build();
|
||||
ResponseBody copy = ResponseBody.create(responseBody.contentType(), content);
|
||||
if (BuildConfig.DEBUG) {
|
||||
if (content.length() < 10240) {
|
||||
Log.e(TAG, "请求体返回:| Response: " + request.url() + "\t body: " + content);
|
||||
}else {
|
||||
Log.e(TAG, "请求体返回:| Response: " + request.url() + "\t body: too long" );
|
||||
}
|
||||
clearExpiredRecords();
|
||||
|
||||
RepeatRecord record = acquireRecord(requestKey);
|
||||
if (record == null) {
|
||||
// 已有相同请求,但没有拿到可用结果(上一次失败、超时或结果过大),本次请求直接访问服务器
|
||||
return chain.proceed(request);
|
||||
}
|
||||
//相同的请求
|
||||
String requestKey = MD5Util.getUpperMD5Str(request.method() + request.url().toString() + requestBodyToString(request.body()));
|
||||
long time = System.currentTimeMillis();//请求时间
|
||||
if (!record.isOwner()) {
|
||||
// 复用相同请求的结果:服务端只会收到一次请求
|
||||
log("复用请求:", requestKey, request, bodyText);
|
||||
return record.newResponse(request);
|
||||
}
|
||||
|
||||
log("注册请求:", requestKey, request, bodyText);
|
||||
try {
|
||||
if (requestIdsMap.size() > 0 && requestIdsMap.containsKey(requestKey)) {
|
||||
log("重复请求:", requestKey, request);
|
||||
//下面这行写了不会抛出onerror
|
||||
// chain.call().cancel();
|
||||
return new Response.Builder()
|
||||
.protocol(Protocol.get(REPEAT_REQUEST_PROTOCOL))
|
||||
.request(request) //multi thread
|
||||
.build();
|
||||
Response response = chain.proceed(request);
|
||||
ResponseBody responseBody = response.body();
|
||||
if (responseBody == null || responseBody.contentLength() > MAX_DEDUP_BODY_BYTES) {
|
||||
// 无响应体或响应体过大:不消费 body,也不参与去重,直接透传
|
||||
logResponse(request, null);
|
||||
return response;
|
||||
}
|
||||
requestIdsMap.put(requestKey, time);
|
||||
log("注册请求:", requestKey, request);
|
||||
// RepeatRequestInterceptor.Builder builder = request.newBuilder();
|
||||
// builder.addHeader("header", jsonObject.toString());
|
||||
return response.newBuilder().body(copy).build();
|
||||
|
||||
// body 只能被消费一次,读出来之后重建一个新的 body 返回给调用方
|
||||
MediaType contentType = responseBody.contentType();
|
||||
byte[] bytes = responseBody.bytes();
|
||||
logResponse(request, bytes);
|
||||
if (bytes.length <= MAX_DEDUP_BODY_BYTES) {
|
||||
// 缓存结果,供等待中的、以及短时间内后续的相同请求复用
|
||||
record.complete(response, bytes, contentType);
|
||||
}
|
||||
return response.newBuilder().body(ResponseBody.create(contentType, bytes)).build();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "intercept: " + e.getMessage());
|
||||
throw e;
|
||||
} finally {
|
||||
if (requestIdsMap.containsKey(requestKey) && requestIdsMap.containsValue(time)) {//请求任务完成删除map中的数据
|
||||
requestIdsMap.remove(requestKey);
|
||||
log("移除请求:", requestKey, request);
|
||||
// 兜底:无论成功、失败还是异常,都结束记录,避免等待中的相同请求被一直阻塞
|
||||
record.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求对应的去重记录。
|
||||
*
|
||||
* @return 返回记录表示本次请求要么成为 owner 去访问服务器、要么直接复用已有结果;
|
||||
* 返回 null 表示已有相同请求但结果不可用,本次请求自己访问服务器(不参与去重)
|
||||
*/
|
||||
private RepeatRecord acquireRecord(String requestKey) {
|
||||
while (true) {
|
||||
long now = System.currentTimeMillis();
|
||||
RepeatRecord record = recordMap.get(requestKey);
|
||||
if (record == null) {
|
||||
RepeatRecord created = new RepeatRecord();
|
||||
if (recordMap.putIfAbsent(requestKey, created) == null) {
|
||||
created.markOwner();
|
||||
return created;
|
||||
}
|
||||
continue;// 其他线程抢先注册,重新判断
|
||||
}
|
||||
if (record.isExpired(now)) {
|
||||
// 结果已过期,替换为本次请求的记录并成为 owner
|
||||
RepeatRecord created = new RepeatRecord();
|
||||
if (recordMap.replace(requestKey, record, created)) {
|
||||
created.markOwner();
|
||||
return created;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 已有相同请求:等待它的结果,拿到结果直接复用
|
||||
if (record.await(WAIT_IN_FLIGHT_TIMEOUT_MS)) {
|
||||
return record;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期记录,释放其缓存的响应体
|
||||
*/
|
||||
private void clearExpiredRecords() {
|
||||
if (recordMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
for (Map.Entry<String, RepeatRecord> entry : recordMap.entrySet()) {
|
||||
RepeatRecord record = entry.getValue();
|
||||
if (record.isExpired(now)) {
|
||||
recordMap.remove(entry.getKey(), record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String action, String requestKey, Request request) {
|
||||
private static String buildRequestKey(Request request, String bodyText) {
|
||||
return MD5Util.getUpperMD5Str(request.method() + "|" + request.url() + "|" + bodyText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求体转字符串,用于计算去重 key 和输出日志。
|
||||
* 只读取可重复读写的请求体,避免把一次性(one-shot)请求体读空。
|
||||
*/
|
||||
private static String requestBodyToString(RequestBody body) {
|
||||
if (body == null || body.isOneShot()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
Buffer buffer = new Buffer();
|
||||
body.writeTo(buffer);
|
||||
long size = buffer.size();
|
||||
if (size > MAX_BODY_BYTES_FOR_KEY) {
|
||||
// 请求体过大时只取前一部分参与去重,避免不必要的内存开销
|
||||
return size + ":" + buffer.readByteString(MAX_BODY_BYTES_FOR_KEY).hex();
|
||||
}
|
||||
return buffer.readUtf8();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static void log(String action, String requestKey, Request request, String bodyText) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method() + " --- " + " URL = " + request.url().encodedPath() + "\t" + bodyToString(request));
|
||||
String body = bodyText.length() > MAX_LOG_BODY_LENGTH ? "-too long" : bodyText;
|
||||
Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method()
|
||||
+ " --- " + " URL = " + request.url().encodedPath() + "\t" + body);
|
||||
} else {
|
||||
Log.e("REPEAT-REQUEST", action + requestKey + " Method @" + request.method());
|
||||
}
|
||||
}
|
||||
|
||||
private static String bodyToString(final Request request) {
|
||||
try {
|
||||
final Request copy = request.newBuilder().build();
|
||||
final Buffer buffer = new Buffer();
|
||||
copy.body().writeTo(buffer);
|
||||
if (buffer.size() > 10240) {
|
||||
return "-too long";
|
||||
}
|
||||
return buffer.readUtf8();
|
||||
} catch (Exception e) {
|
||||
return "-";
|
||||
private static void logResponse(Request request, byte[] bytes) {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
return;
|
||||
}
|
||||
if (bytes == null || bytes.length > MAX_LOG_BODY_LENGTH) {
|
||||
Log.e(TAG, "请求体返回:| Response: " + request.url() + "\t body: too long");
|
||||
} else {
|
||||
Log.e(TAG, "请求体返回:| Response: " + request.url()
|
||||
+ "\t body: " + new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private static String requestBodyToString(RequestBody body) {
|
||||
try {
|
||||
final Buffer buffer = new Buffer();
|
||||
body.writeTo(buffer);
|
||||
if (buffer.size() > 10240) {
|
||||
return "-too long";
|
||||
/**
|
||||
* 一次请求的去重记录:owner 线程负责访问服务器,其他相同请求等待并复用它的结果
|
||||
*/
|
||||
private static final class RepeatRecord {
|
||||
private final Object lock = new Object();
|
||||
private final long createdTime = System.currentTimeMillis();
|
||||
|
||||
private boolean owner;
|
||||
private boolean finished;
|
||||
private boolean succeed;
|
||||
private long finishedTime;
|
||||
private byte[] body;
|
||||
private MediaType contentType;
|
||||
private Headers headers;
|
||||
private Protocol protocol;
|
||||
private int code;
|
||||
private String message;
|
||||
|
||||
void markOwner() {
|
||||
owner = true;
|
||||
}
|
||||
|
||||
boolean isOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 owner 请求结束
|
||||
*
|
||||
* @return true 表示已拿到可复用的结果
|
||||
*/
|
||||
boolean await(long timeoutMs) {
|
||||
synchronized (lock) {
|
||||
if (!finished) {
|
||||
try {
|
||||
lock.wait(timeoutMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
return finished && succeed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功,缓存结果并唤醒等待的请求
|
||||
*/
|
||||
void complete(Response response, byte[] body, MediaType contentType) {
|
||||
synchronized (lock) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
this.body = body;
|
||||
this.contentType = contentType;
|
||||
this.headers = response.headers();
|
||||
this.protocol = response.protocol();
|
||||
this.code = response.code();
|
||||
this.message = response.message();
|
||||
this.succeed = true;
|
||||
finished = true;
|
||||
finishedTime = System.currentTimeMillis();
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求结束(失败或异常时没有可复用的结果);重复调用忽略
|
||||
*/
|
||||
void finish() {
|
||||
synchronized (lock) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
finishedTime = System.currentTimeMillis();
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行中的记录超过等待超时时间、已完成的记录超过结果保留时间即视为过期
|
||||
*/
|
||||
boolean isExpired(long now) {
|
||||
synchronized (lock) {
|
||||
if (!finished) {
|
||||
return now - createdTime > WAIT_IN_FLIGHT_TIMEOUT_MS;
|
||||
}
|
||||
return now - finishedTime > REUSE_WINDOW_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用缓存的结果构建响应,状态码、响应头、响应体都是真实数据
|
||||
*/
|
||||
Response newResponse(Request request) {
|
||||
synchronized (lock) {
|
||||
return new Response.Builder()
|
||||
.request(request)
|
||||
.protocol(protocol)
|
||||
.code(code)
|
||||
.message(message)
|
||||
.headers(headers)
|
||||
.body(ResponseBody.create(contentType, body))
|
||||
.build();
|
||||
}
|
||||
return buffer.readUtf8();
|
||||
} catch (Exception e) {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.aoleyun.sn.manager.ConnectManager;
|
||||
import com.aoleyun.sn.rlog.LogDBManager;
|
||||
import com.aoleyun.sn.utils.BootManager;
|
||||
import com.aoleyun.sn.utils.JgyUtils;
|
||||
import com.aoleyun.sn.utils.PowerOffAlarmUtils;
|
||||
import com.aoleyun.sn.utils.SPUtils;
|
||||
import com.aoleyun.sn.utils.TimeUtils;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
@@ -77,6 +78,8 @@ public class BootReceiver extends BroadcastReceiver {
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
mMMKV.encode(CommonConfig.DEVICES_REBOOT, true);
|
||||
mMMKV.encode(ConnectManager.REBOOT_LAST_ONNECT_TIME, System.currentTimeMillis());
|
||||
//设备重启后 AlarmManager 中的闹钟会被系统清空,重新安排定时关机
|
||||
PowerOffAlarmUtils.schedule(context);
|
||||
if ((int) SPUtils.get(context, CommonConfig.FIRST_STARTUP, 0) == 0) {
|
||||
LogDBManager.getInstance().creatRebootLog("首次启动", TimeUtils.transferLongToDate(System.currentTimeMillis()));
|
||||
SPUtils.put(context, CommonConfig.FIRST_STARTUP, 1);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.aoleyun.sn.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.aoleyun.sn.utils.JgyUtils;
|
||||
import com.aoleyun.sn.utils.PowerOffAlarmUtils;
|
||||
|
||||
/**
|
||||
* 定时关机广播接收器
|
||||
* <p>
|
||||
* 由 {@link PowerOffAlarmUtils} 通过 AlarmManager 的精确闹钟触发。
|
||||
* 使用 manifest 静态注册,应用进程被杀掉后依然能被系统拉起并执行关机。
|
||||
*
|
||||
* @author aoleyun
|
||||
*/
|
||||
public class PowerOffReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "PowerOffReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = null == intent ? null : intent.getAction();
|
||||
Log.e(TAG, "onReceive: " + action);
|
||||
if (!PowerOffAlarmUtils.ACTION_POWER_OFF_ALARM.equals(action)) {
|
||||
return;
|
||||
}
|
||||
//先安排下一次(循环定时关机),再关机,避免关机过程中闹钟安排失败
|
||||
PowerOffAlarmUtils.schedule(context);
|
||||
Log.e(TAG, "onReceive: 设备关机");
|
||||
JgyUtils.getInstance().shutdown();
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,6 @@ import com.aoleyun.sn.BuildConfig;
|
||||
import com.aoleyun.sn.R;
|
||||
import com.aoleyun.sn.activity.main.MainActivity;
|
||||
import com.aoleyun.sn.base.BaseService;
|
||||
import com.aoleyun.sn.bean.PoweroffBean;
|
||||
import com.aoleyun.sn.bean.ScreenLockState;
|
||||
import com.aoleyun.sn.comm.CommonConfig;
|
||||
import com.aoleyun.sn.comm.JGYActions;
|
||||
@@ -56,24 +55,21 @@ import com.aoleyun.sn.utils.ApkUtils;
|
||||
import com.aoleyun.sn.utils.CacheUtils;
|
||||
import com.aoleyun.sn.utils.ForegroundAppUtil;
|
||||
import com.aoleyun.sn.utils.JgyUtils;
|
||||
import com.aoleyun.sn.utils.PowerOffAlarmUtils;
|
||||
import com.aoleyun.sn.utils.SPUtils;
|
||||
import com.aoleyun.sn.utils.SysSettingUtils;
|
||||
import com.aoleyun.sn.utils.TimeUtils;
|
||||
import com.aoleyun.sn.utils.Utils;
|
||||
import com.arialyy.aria.core.Aria;
|
||||
import com.blankj.utilcode.util.NetworkUtils;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.hjq.toast.Toaster;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
@@ -127,6 +123,8 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
|
||||
super.onPostExecute(aLong);
|
||||
AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||||
am.setTime(aLong);
|
||||
//系统时间被修改后重新安排定时关机闹钟
|
||||
PowerOffAlarmUtils.schedule(MainService.this);
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -444,6 +442,8 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
|
||||
}
|
||||
SPUtils.put(this, CommonConfig.SN_VERSIONCODE, BuildConfig.VERSION_CODE);
|
||||
|
||||
//根据本地保存的策略安排定时关机闹钟(服务重启、设备重启后重新计算)
|
||||
PowerOffAlarmUtils.schedule(this);
|
||||
registerReceivers();
|
||||
NetworkUtils.registerNetworkStatusChangedListener(this);
|
||||
timeChangedStart.onstar(System.currentTimeMillis());
|
||||
@@ -1035,7 +1035,8 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
|
||||
case Intent.ACTION_TIME_CHANGED:
|
||||
case Intent.ACTION_TIMEZONE_CHANGED:
|
||||
case Intent.ACTION_TIME_TICK:
|
||||
checkShutdownTime();
|
||||
//时间被修改(手动修改/NTP 校时)后重新安排定时关机闹钟
|
||||
PowerOffAlarmUtils.schedule(MainService.this);
|
||||
checkUploadLogTime();
|
||||
setFloatingWindow();
|
||||
JgyUtils.getInstance().checkTimePeriod();
|
||||
@@ -1049,29 +1050,6 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
|
||||
}
|
||||
}
|
||||
|
||||
private void checkShutdownTime() {
|
||||
String poweroffTime = mMMKV.decodeString("poweroffTime", "");
|
||||
Log.e("checkShutdownTime", "poweroffTime: " + poweroffTime);
|
||||
Gson gson = new Gson();
|
||||
Type listType = new TypeToken<List<PoweroffBean>>() {
|
||||
}.getType();
|
||||
List<PoweroffBean> poweroffBeanList = gson.fromJson(poweroffTime, listType);
|
||||
if (poweroffBeanList != null && poweroffBeanList.size() != 0) {
|
||||
boolean inTime = false;
|
||||
for (PoweroffBean poweroffBean : poweroffBeanList) {
|
||||
if (TimeUtils.isShutdownTime(poweroffBean)) {
|
||||
inTime = true;
|
||||
}
|
||||
}
|
||||
if (inTime) {
|
||||
JgyUtils.getInstance().shutdown();
|
||||
Log.e(TAG, "checkShutdownTime: " + "设备关机");
|
||||
} else {
|
||||
Log.e(TAG, "checkShutdownTime: " + "没有关机事件");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DisplayReceiver mDisplayReceiver;
|
||||
|
||||
public void registerDisplayReceiver() {
|
||||
|
||||
@@ -1447,7 +1447,7 @@ public class ApkUtils {
|
||||
if (factoryapp.contains(packageName) && !G30_APP.contains(packageName)) {
|
||||
continue;
|
||||
}
|
||||
//排除所有系统应用,不显示
|
||||
//排除系统应用,不显示
|
||||
if (isSystemApp(context, packageName)) {
|
||||
if (!AoleyunOSApp.contains(packageName)
|
||||
&& !aoleyunAPP.contains(packageName)
|
||||
@@ -1456,6 +1456,8 @@ public class ApkUtils {
|
||||
&& !yixuepaiApp.contains(packageName)
|
||||
&& !mJxwApp.contains(packageName)
|
||||
&& !G30_APP.contains(packageName)
|
||||
&& !DX11App.contains(packageName)
|
||||
&& !MX11App.contains(packageName)
|
||||
&& !allPkgSet.contains(packageName)
|
||||
) {
|
||||
Log.e(TAG, "getRunningAppInfo system: continue = " + packageName);
|
||||
|
||||
144
app/src/main/java/com/aoleyun/sn/utils/PowerOffAlarmUtils.java
Normal file
144
app/src/main/java/com/aoleyun/sn/utils/PowerOffAlarmUtils.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.aoleyun.sn.utils;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.aoleyun.sn.bean.PoweroffBean;
|
||||
import com.aoleyun.sn.comm.CommonConfig;
|
||||
import com.aoleyun.sn.receiver.PowerOffReceiver;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.tencent.mmkv.MMKV;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 定时关机闹钟管理类
|
||||
* <p>
|
||||
* 采用官方推荐的 {@link AlarmManager} 精确闹钟(RTC_WAKEUP + setExactAndAllowWhileIdle)实现定时关机,
|
||||
* 只在需要的时间点唤醒系统,而不是靠 ACTION_TIME_TICK 每分钟轮询:
|
||||
* 1. 设备休眠(Doze)时依然能准时触发;
|
||||
* 2. 系统时间被修改(手动修改时间、NTP 校时)后重新计算并安排;
|
||||
* 3. 设备重启后由 {@link com.aoleyun.sn.receiver.BootReceiver} 重新拉起服务并重新安排;
|
||||
* 4. 服务器下发新的关机策略后重新安排。
|
||||
*
|
||||
* @author aoleyun
|
||||
*/
|
||||
public final class PowerOffAlarmUtils {
|
||||
|
||||
private static final String TAG = "PowerOffAlarmUtils";
|
||||
|
||||
/**
|
||||
* 服务器下发的定时关机策略在 MMKV 中的存储 key
|
||||
*/
|
||||
public static final String KEY_POWEROFF_TIME = "poweroffTime";
|
||||
|
||||
/**
|
||||
* 定时关机闹钟触发时发送的广播
|
||||
*/
|
||||
public static final String ACTION_POWER_OFF_ALARM = "com.aoleyun.sn.action.POWER_OFF_ALARM";
|
||||
|
||||
/**
|
||||
* 闹钟的唯一标识,用于取消/覆盖已有的闹钟
|
||||
*/
|
||||
private static final int REQUEST_CODE = 1001;
|
||||
|
||||
private PowerOffAlarmUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存服务器下发的定时关机策略,key 与 {@link #schedule(Context)} 保持一致
|
||||
*/
|
||||
public static void savePowerOffTime(String poweroffTime) {
|
||||
String value = TextUtils.isEmpty(poweroffTime) ? "" : poweroffTime;
|
||||
Log.e(TAG, "savePowerOffTime: " + value);
|
||||
getMMKV().encode(KEY_POWEROFF_TIME, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本地保存的关机策略安排下一次定时关机闹钟,没有可用策略时会取消已有的闹钟。
|
||||
* 该方法可以重复调用,用于时间被修改、设备重启、策略更新等场景下的重新安排。
|
||||
*/
|
||||
public static void schedule(Context context) {
|
||||
if (null == context) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (null == appContext) {
|
||||
return;
|
||||
}
|
||||
AlarmManager alarmManager = (AlarmManager) appContext.getSystemService(Context.ALARM_SERVICE);
|
||||
if (null == alarmManager) {
|
||||
Log.e(TAG, "schedule: AlarmManager is null");
|
||||
return;
|
||||
}
|
||||
|
||||
PendingIntent pendingIntent = buildPendingIntent(appContext);
|
||||
long triggerAtMillis = getNextPowerOffTime();
|
||||
if (triggerAtMillis <= System.currentTimeMillis()) {
|
||||
alarmManager.cancel(pendingIntent);
|
||||
Log.e(TAG, "schedule: 没有可用的定时关机策略,已取消闹钟");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// RTC_WAKEUP 使用系统墙钟时间,设备休眠时同样会唤醒并触发
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
|
||||
Log.e(TAG, "schedule: 下一次定时关机时间 = " + TimeUtils.transferLongToDate(triggerAtMillis));
|
||||
} catch (SecurityException e) {
|
||||
// 精确闹钟权限不可用时降级为非精确闹钟,保证关机策略依然生效
|
||||
Log.e(TAG, "schedule: 精确闹钟不可用,降级为非精确闹钟: " + e.getMessage());
|
||||
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 所有关机策略中最近的一次触发时间戳,没有可用策略时返回 0
|
||||
*/
|
||||
public static long getNextPowerOffTime() {
|
||||
List<PoweroffBean> poweroffBeanList = getPowerOffTimeList();
|
||||
if (null == poweroffBeanList || poweroffBeanList.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
long nowTime = System.currentTimeMillis();
|
||||
long nextTime = Long.MAX_VALUE;
|
||||
for (PoweroffBean poweroffBean : poweroffBeanList) {
|
||||
long shutdownTime = TimeUtils.getNextShutdownTime(poweroffBean);
|
||||
if (shutdownTime > nowTime && shutdownTime < nextTime) {
|
||||
nextTime = shutdownTime;
|
||||
}
|
||||
}
|
||||
return nextTime == Long.MAX_VALUE ? 0 : nextTime;
|
||||
}
|
||||
|
||||
public static List<PoweroffBean> getPowerOffTimeList() {
|
||||
String poweroffTime = getMMKV().decodeString(KEY_POWEROFF_TIME, "");
|
||||
Log.e(TAG, "getPowerOffTimeList: " + poweroffTime);
|
||||
if (TextUtils.isEmpty(poweroffTime)) {
|
||||
return null;
|
||||
}
|
||||
Type listType = new TypeToken<List<PoweroffBean>>() {
|
||||
}.getType();
|
||||
return new Gson().fromJson(poweroffTime, listType);
|
||||
}
|
||||
|
||||
private static PendingIntent buildPendingIntent(Context context) {
|
||||
Intent intent = new Intent(context, PowerOffReceiver.class);
|
||||
intent.setAction(ACTION_POWER_OFF_ALARM);
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flags);
|
||||
}
|
||||
|
||||
private static MMKV getMMKV() {
|
||||
return MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE);
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,13 @@ package com.aoleyun.sn.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.aoleyun.sn.bean.PoweroffBean;
|
||||
import com.aoleyun.sn.service.main.MainService;
|
||||
@@ -24,6 +22,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -51,26 +50,75 @@ public class TimeUtils {
|
||||
//
|
||||
// }
|
||||
|
||||
public static boolean isShutdownTime(PoweroffBean poweroffBean) {
|
||||
int type = poweroffBean.getType();
|
||||
if (type == 1) { //单次定时关机
|
||||
return isShutdownTime(poweroffBean.getTime());
|
||||
} else if (type == 2) {//循环定时关机
|
||||
return isShutdownTime(getZeroTiemstamp() + getLoopTime(poweroffBean.getTime()));
|
||||
/**
|
||||
* 单次定时关机
|
||||
*/
|
||||
public static final int TYPE_SINGLE = 1;
|
||||
/**
|
||||
* 循环定时关机(每天重复)
|
||||
*/
|
||||
public static final int TYPE_LOOP = 2;
|
||||
|
||||
/**
|
||||
* 计算定时关机的下一次触发时间
|
||||
*
|
||||
* @param poweroffBean 服务器下发的关机策略
|
||||
* @return 下一次触发的时间戳,策略无效时返回 -1
|
||||
*/
|
||||
public static long getNextShutdownTime(PoweroffBean poweroffBean) {
|
||||
if (null == poweroffBean || TextUtils.isEmpty(poweroffBean.getTime())) {
|
||||
return -1;
|
||||
}
|
||||
return false;
|
||||
int type = poweroffBean.getType();
|
||||
if (type == TYPE_SINGLE) {
|
||||
return getSingleShutdownTime(poweroffBean.getTime());
|
||||
} else if (type == TYPE_LOOP) {
|
||||
return getLoopShutdownTime(poweroffBean.getTime());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取今日0点时间戳
|
||||
* 没有管时区问题
|
||||
* 单次定时关机,时间格式:yyyy-MM-dd HH:mm:ss
|
||||
*/
|
||||
private static long getZeroTiemstamp() {
|
||||
long nowTime = System.currentTimeMillis();
|
||||
long dayMillisecond = 60 * 60 * 24 * 1000;
|
||||
long zeroTime = ((nowTime) / dayMillisecond) * dayMillisecond;
|
||||
Log.e(TAG, "getZeroTiemstamp: " + zeroTime);
|
||||
return zeroTime;
|
||||
private static long getSingleShutdownTime(String time) {
|
||||
//"2021-11-18 00:00:00"
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
try {
|
||||
Date date = sdf.parse(time.trim());
|
||||
return null == date ? -1 : date.getTime();
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "getSingleShutdownTime: 时间格式错误: " + time);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环定时关机,时间格式:HH:mm:ss,每天重复
|
||||
* 今天的时间点已经过去则顺延到明天
|
||||
*/
|
||||
private static long getLoopShutdownTime(String time) {
|
||||
//"15:34:39"
|
||||
String[] times = time.trim().split(":");
|
||||
if (times.length < 2) {
|
||||
Log.e(TAG, "getLoopShutdownTime: 时间格式错误: " + time);
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(times[0].trim()));
|
||||
calendar.set(Calendar.MINUTE, Integer.parseInt(times[1].trim()));
|
||||
calendar.set(Calendar.SECOND, times.length > 2 ? Integer.parseInt(times[2].trim()) : 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
|
||||
calendar.add(Calendar.DAY_OF_YEAR, 1);
|
||||
}
|
||||
Log.e(TAG, "getLoopShutdownTime: " + transferLongToDate(calendar.getTimeInMillis()));
|
||||
return calendar.getTimeInMillis();
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "getLoopShutdownTime: 时间格式错误: " + time);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getPhotoDate() {
|
||||
@@ -102,47 +150,6 @@ public class TimeUtils {
|
||||
return sdf.format(date);
|
||||
}
|
||||
|
||||
private static long getLoopTime(String timestamp) {
|
||||
//"15:34:39"
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
|
||||
try {
|
||||
Date d = sdf.parse(timestamp);
|
||||
long loopTime = d.getTime();
|
||||
Log.e(TAG, "getLoopTime: LoopTime = " + loopTime);
|
||||
return loopTime;
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "getLoopTime: e: " + e.getStackTrace());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isShutdownTime(String timestamp) {
|
||||
//"2021-11-18 00:00:00"
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
try {
|
||||
Date d = sdf.parse(timestamp);
|
||||
long realTime = d.getTime();
|
||||
return isShutdownTime(realTime);
|
||||
} catch (ParseException e) {
|
||||
Log.e(TAG, "isShutdownTime: " + e.getStackTrace());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isShutdownTime(long timestamp) {
|
||||
long realTime = timestamp;
|
||||
long nowTime = System.currentTimeMillis();
|
||||
if (String.valueOf(timestamp).length() < String.valueOf(nowTime).length()) {
|
||||
realTime = timestamp * 1000;
|
||||
}
|
||||
if (realTime >= nowTime && realTime <= (nowTime + 1000 * 60)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否在管控时间内
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user