原实现是在 {@code chain.proceed()} 之后才判断是否为重复请求,此时请求早已发到服务器, + * 只能丢弃返回结果,所以服务端依然会收到两次(甚至多次)请求;并且记录在 finally 中立即删除, + * 去重窗口几乎为零,重复请求根本拦不住。
+ * + *优化后的策略:
+ *v1.0 2022-07-15 16:16:52
*/ public class RepeatRequestInterceptor implements Interceptor { private static final String TAG = "RepeatRequestInterceptor"; - private final ConcurrentHashMap
+ * 由 {@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();
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/service/main/MainService.java b/app/src/main/java/com/aoleyun/sn/service/main/MainService.java
index 0825928..2e61138 100644
--- a/app/src/main/java/com/aoleyun/sn/service/main/MainService.java
+++ b/app/src/main/java/com/aoleyun/sn/service/main/MainService.java
@@ -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
+ * 采用官方推荐的 {@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>() {
- }.getType();
- List
>() {
+ }.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);
+ }
+}
diff --git a/app/src/main/java/com/aoleyun/sn/utils/TimeUtils.java b/app/src/main/java/com/aoleyun/sn/utils/TimeUtils.java
index beb588e..ea9ef49 100644
--- a/app/src/main/java/com/aoleyun/sn/utils/TimeUtils.java
+++ b/app/src/main/java/com/aoleyun/sn/utils/TimeUtils.java
@@ -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;
- }
- }
-
-
/**
* 是否在管控时间内
*