摘要:本文主要介绍一种定时重启设备的方案,目的是对于低内存设备,防止其由于长时间不关机且应用消耗过多RAM导致卡顿甚至无法开机的现象。基本方案是通过AlarmManager设置定时闹钟唤醒系统并且判断是否符合重启条件,从而进行弹框提醒和倒计时重启。
由于是系统定制功能,所以设计之初定位在Settings模块中实现
patch代码说明:
AndroidManifest.xml创建receiver并且静态注册监听开机广播android.intent.action.BOOT_COMPLETED
Index: packages/apps/Settings/AndroidManifest.xml
===================================================================
--- packages/apps/Settings/AndroidManifest.xml (版本 10656)
+++ packages/apps/Settings/AndroidManifest.xml (版本 10657)
@@ -3594,5 +3594,14 @@
+
+
+
+
+
+
+
设置闹钟:需求是设备运行时间超过48小时后的每天2点定时重启
REBOOT_CANCEL_COUNT是用来记录取消重启Dialog的次数,可以取消2次,第三次弹框只有重启倒计时按钮,不显示取消按钮,所以监听到开机广播后需要重置记录的次数为0。
cancelIntervalAlarm是取消点击1次或者2次Dialog中取消重启按钮后,重启设备会持续执行的闹钟。
cancelAlarm是取消之前定时2点的闹钟
setAlarm是重新设置闹钟
关于设置闹钟非常重要的是,
1)设置的循环闹钟早于当前系统时间,会立即(差不多1分钟左右,主要根据系统性能)触发一次闹钟事件,解决方案就是判断设置的时间早于当前系统时间需要加24小时。
2)闹钟类型必须是RTC_WAKEUP,会唤醒系统,为是否弹框做准备。
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "action = " + intent.getAction());
+
+ if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ // reset hint count to 0
+ Settings.Global.putInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, 0);
+ cancelIntervalAlarm(context);
+ cancelAlarm(context);
+ setAlarm(context);
+ }
+ }
+
+ private void setAlarm(Context context) {
+ Intent intent = new Intent(ALARM_1);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(System.currentTimeMillis());
+ calendar.set(Calendar.HOUR_OF_DAY, ALARM_HOURS);
+ calendar.set(Calendar.MINUTE, ALARM_MINUTES);
+ calendar.set(Calendar.SECOND, ALARM_SECONDS);
+ long systemTime = System.currentTimeMillis();
+ long selectTime = calendar.getTimeInMillis();
+ if (systemTime > selectTime) {
+ calendar.add(Calendar.HOUR_OF_DAY, 24);
+ }
+ alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent);
+ }
+
+ private static void cancelAlarm(Context context) {
+ Intent intent = new Intent(ALARM_1);
+ PendingIntent pendingIntent= PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pendingIntent);
+ }
+
+ private void setIntervalAlarm(Context context) {
+ Intent intent = new Intent(ALARM_2);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + INTERVAL_TIME, pendingIntent);
+ }
+
+ private static void cancelIntervalAlarm(Context context) {
+ Intent intent = new Intent(ALARM_2);
+ PendingIntent pendingIntent= PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pendingIntent);
+ }
整点2点闹钟触发后,判断设备运行时间是否大于48小时,若大于则亮屏弹框,否则不做任何处理。
闹钟触发的广播ALARM_1是在setAlarm中设置的,并且已在AndroidManifest.xml中静态注册。
触发后计算设备运行时间Up time,此方法可参考frameworks\base\core\java\android\text\format\DateUtils.java中的formatElapsedTime方法。
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "action = " + intent.getAction());
+
+ if (ALARM_1.equals(intent.getAction())) {
+ long upTimeHours = 0;
+ long upTimeMinutes = 0;
+ long upTimeSeconds = 0;
+ long elapsedSeconds = SystemClock.elapsedRealtime() / 1000;
+ if (elapsedSeconds >= 3600) {
+ upTimeHours = elapsedSeconds / 3600;
+ elapsedSeconds -= upTimeHours * 3600;
+ }
+ if (elapsedSeconds >= 60) {
+ upTimeMinutes = elapsedSeconds / 60;
+ elapsedSeconds -= upTimeMinutes * 60;
+ }
+ upTimeSeconds = elapsedSeconds;
+ boolean eligible = upTimeHours >= UP_TIME_THRESHOLD; // 48h
+ Log.d(TAG, "onReceive::up time = " + upTimeHours + ":" + upTimeMinutes + ":" + upTimeSeconds + ", eligible = " + eligible);
+ if (eligible) {
+ showRebootDialog(context);
+ }
+ }
+ }
点击取消按钮,每隔1小时循环弹框2次,若2次都取消,第三次弹框不显示取消按钮。
AlertWakeLock.acquireScreenBrightWakeLock(context);是在设备灭屏或者深睡眠状态被RTC_WAKEUP闹钟唤醒后,自动亮屏。
rebootCancelCount记录取消的次数,以此来判定是否显示取消按钮。
若点击重启按钮,则直接重启;
若点击取消按钮,取消次数+1,并且设置1小时间隔的循环闹钟,此闹钟需要注意的是,
1) 闹钟类型必须是ELAPSED_REALTIME_WAKEUP,会唤醒系统,并且时间参数是实际的间隔时间
+ private void showRebootDialog(Context context) {
+ AlertWakeLock.acquireScreenBrightWakeLock(context);
+ int rebootCancelCount = Settings.Global.getInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, 0);
+ boolean forceReboot = rebootCancelCount == CANCEL_TIMES;
+ Log.d(TAG, "showRebootDialog::rebootCancelCount = " + rebootCancelCount + ", forceReboot = " + forceReboot);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.reboot);
+ builder.setMessage(R.string.reboot_msg);
+ builder.setPositiveButton(R.string.reboot, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ AlertWakeLock.releaseScreenBrightWakeLock();
+ reboot();
+ }
+ });
+ if (!forceReboot) {
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ AlertWakeLock.releaseScreenBrightWakeLock();
+ int count = Settings.Global.getInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, 0);
+ Settings.Global.putInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, count + 1);
+ cancelIntervalAlarm(context);
+ setIntervalAlarm(context);
+ }
+ });
+ }
+ AlertDialog alertDialog = builder.create();
+ DialogTimeoutListener listener = new DialogTimeoutListener();
+ alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ alertDialog.setCancelable(false);
+ alertDialog.setOnShowListener(listener);
+ alertDialog.setOnDismissListener(listener);
+ alertDialog.show();
+ }
此触发很简单,直接弹框即可
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "action = " + intent.getAction());
+
+ if (ALARM_2.equals(intent.getAction())) {
+ showRebootDialog(context);
+ }
+ }
重启接口很实用。
+ private void reboot() {
+ IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+ try {
+ pm.reboot(false, "reboot", true);
+ } catch (RemoteException e) {
+ }
+ }
Index: packages/apps/Settings/src/com/android/settings/AutoRebootReceiver.java
===================================================================
--- packages/apps/Settings/src/com/android/settings/AutoRebootReceiver.java (不存在的)
+++ packages/apps/Settings/src/com/android/settings/AutoRebootReceiver.java (版本 10657)
@@ -0,0 +1,164 @@
+package com.android.settings;
+
+import android.app.AlarmManager;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.IPowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.android.settings.R;
+import com.android.settings.utils.AlertWakeLock;
+import com.android.settings.utils.DialogTimeoutListener;
+
+import java.util.Calendar;
+
+/**
+ * 1. The device runs every 48 hours and show dialog at 2 pm at night.
+ * 2. The dialog prompts the user whether to reboot and counts down to 30 seconds.
+ * 3. The device will reboot when click reboot button or countdown ends of 30 seconds.
+ * 4. If click cancel button, the dialog will be displayed every 1 hour.
+ * 5. Only show reboot button and counts down to 30 seconds after cancel twice.
+ */
+public class AutoRebootReceiver extends BroadcastReceiver {
+ private static String TAG ="AutoRebootReceiver";
+
+ private static final String ALARM_1 = "XXX.XXX.XXX.XXX.XXX_ALARM_1";
+ private static final String ALARM_2 = "XXX.XXX.XXX.XXX.XXX_ALARM_2";
+
+ private static final int ALARM_HOURS = 2;
+ private static final int ALARM_MINUTES = 0;
+ private static final int ALARM_SECONDS = 0;
+ private static final int INTERVAL_TIME = 60 * 60 * 1000; // 1h
+ private static final int CANCEL_TIMES = 3;
+ private static final long UP_TIME_THRESHOLD = 48; // 48h
+
+ private static final String REBOOT_CANCEL_COUNT = "reboot_cancel_count";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "action = " + intent.getAction());
+
+ if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ // reset hint count to 0
+ Settings.Global.putInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, 0);
+ cancelIntervalAlarm(context);
+ cancelAlarm(context);
+ setAlarm(context);
+ }
+
+ if (ALARM_1.equals(intent.getAction())) {
+ long upTimeHours = 0;
+ long upTimeMinutes = 0;
+ long upTimeSeconds = 0;
+ long elapsedSeconds = SystemClock.elapsedRealtime() / 1000;
+ if (elapsedSeconds >= 3600) {
+ upTimeHours = elapsedSeconds / 3600;
+ elapsedSeconds -= upTimeHours * 3600;
+ }
+ if (elapsedSeconds >= 60) {
+ upTimeMinutes = elapsedSeconds / 60;
+ elapsedSeconds -= upTimeMinutes * 60;
+ }
+ upTimeSeconds = elapsedSeconds;
+ boolean eligible = upTimeHours >= UP_TIME_THRESHOLD; // 48h
+ Log.d(TAG, "onReceive::up time = " + upTimeHours + ":" + upTimeMinutes + ":" + upTimeSeconds + ", eligible = " + eligible);
+ if (eligible) {
+ showRebootDialog(context);
+ }
+ }
+
+ if (ALARM_2.equals(intent.getAction())) {
+ showRebootDialog(context);
+ }
+ }
+
+ private void setAlarm(Context context) {
+ Intent intent = new Intent(ALARM_1);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(System.currentTimeMillis());
+ calendar.set(Calendar.HOUR_OF_DAY, ALARM_HOURS);
+ calendar.set(Calendar.MINUTE, ALARM_MINUTES);
+ calendar.set(Calendar.SECOND, ALARM_SECONDS);
+ long systemTime = System.currentTimeMillis();
+ long selectTime = calendar.getTimeInMillis();
+ if (systemTime > selectTime) {
+ calendar.add(Calendar.HOUR_OF_DAY, 24);
+ }
+ alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), AlarmManager.INTERVAL_DAY, pendingIntent);
+ }
+
+ private static void cancelAlarm(Context context) {
+ Intent intent = new Intent(ALARM_1);
+ PendingIntent pendingIntent= PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pendingIntent);
+ }
+
+ private void setIntervalAlarm(Context context) {
+ Intent intent = new Intent(ALARM_2);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + INTERVAL_TIME, pendingIntent);
+ }
+
+ private static void cancelIntervalAlarm(Context context) {
+ Intent intent = new Intent(ALARM_2);
+ PendingIntent pendingIntent= PendingIntent.getBroadcast(context, 0, intent, 0);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pendingIntent);
+ }
+
+ private void showRebootDialog(Context context) {
+ AlertWakeLock.acquireScreenBrightWakeLock(context);
+ int rebootCancelCount = Settings.Global.getInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, 0);
+ boolean forceReboot = rebootCancelCount == CANCEL_TIMES;
+ Log.d(TAG, "showRebootDialog::rebootCancelCount = " + rebootCancelCount + ", forceReboot = " + forceReboot);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.reboot);
+ builder.setMessage(R.string.reboot_msg);
+ builder.setPositiveButton(R.string.reboot, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ AlertWakeLock.releaseScreenBrightWakeLock();
+ reboot();
+ }
+ });
+ if (!forceReboot) {
+ builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ AlertWakeLock.releaseScreenBrightWakeLock();
+ int count = Settings.Global.getInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, 0);
+ Settings.Global.putInt(context.getContentResolver(), REBOOT_CANCEL_COUNT, count + 1);
+ cancelIntervalAlarm(context);
+ setIntervalAlarm(context);
+ }
+ });
+ }
+ AlertDialog alertDialog = builder.create();
+ DialogTimeoutListener listener = new DialogTimeoutListener();
+ alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ alertDialog.setCancelable(false);
+ alertDialog.setOnShowListener(listener);
+ alertDialog.setOnDismissListener(listener);
+ alertDialog.show();
+ }
+
+ private void reboot() {
+ IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+ try {
+ pm.reboot(false, "reboot", true);
+ } catch (RemoteException e) {
+ }
+ }
+
+}
此设计非常实用,分享给大家。灵感来源:android中警报框上的倒数计时器
设计理念就是在创建Dialog时设置setOnShowListener和setOnDismissListener,onShow获取重启按钮的文本并且创建倒计时CountDownTimer,在倒计时onTick方法中更新按钮的文本。
Index: packages/apps/Settings/src/com/android/settings/utils/DialogTimeoutListener.java
===================================================================
--- packages/apps/Settings/src/com/android/settings/utils/DialogTimeoutListener.java (不存在的)
+++ packages/apps/Settings/src/com/android/settings/utils/DialogTimeoutListener.java (版本 10657)
@@ -0,0 +1,68 @@
+package com.android.settings.utils;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.CountDownTimer;
+import android.os.IPowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.widget.Button;
+
+import java.util.concurrent.TimeUnit;
+import java.util.Locale;
+
+public class DialogTimeoutListener implements DialogInterface.OnShowListener, DialogInterface.OnDismissListener {
+
+ private static final int AUTO_DISMISS_MILLIS = 30 * 1000; // 30s
+
+ private CountDownTimer mCountDownTimer;
+
+ @Override
+ public void onShow(final DialogInterface dialog) {
+ final Button positiveBtn = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
+ final CharSequence positiveBtnText = positiveBtn.getText();
+ mCountDownTimer = new CountDownTimer(AUTO_DISMISS_MILLIS, 100) {
+ @Override
+ public void onTick(long millisUntilFinished) {
+ if (millisUntilFinished > 60000) {
+ positiveBtn.setText(String.format(
+ Locale.getDefault(), "%s (%d:%02d)",
+ positiveBtnText,
+ TimeUnit.MILLISECONDS.toMinutes(millisUntilFinished),
+ TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished % 60000)
+ ));
+ } else {
+ positiveBtn.setText(String.format(
+ Locale.getDefault(), "%s (%02d)",
+ positiveBtnText,
+ TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished) + 1 //add one so it never displays zero
+ ));
+ }
+ }
+
+ @Override
+ public void onFinish() {
+ if (((AlertDialog) dialog).isShowing()) {
+ // TODO: call your logout method
+ dialog.dismiss();
+ reboot();
+ }
+ }
+ };
+ mCountDownTimer.start();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mCountDownTimer.cancel();
+ }
+
+ private void reboot() {
+ IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+ try {
+ pm.reboot(false, "reboot", true);
+ } catch (RemoteException e) {
+ }
+ }
+}
闹钟触发后只会唤醒系统,并不会亮屏,那Dialog自然也只有亮屏后才会显示,这就需要唤醒系统时亮屏显示Dialog。
添加亮屏唤醒锁即可。
Index: packages/apps/Settings/src/com/android/settings/utils/AlertWakeLock.java
===================================================================
--- packages/apps/Settings/src/com/android/settings/utils/AlertWakeLock.java (不存在的)
+++ packages/apps/Settings/src/com/android/settings/utils/AlertWakeLock.java (版本 10657)
@@ -0,0 +1,40 @@
+package com.android.settings.utils;
+
+import android.content.Context;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+
+public class AlertWakeLock {
+ private static final String TAG = "AlertWakeLock";
+
+ private static final long MAX_SCREEN_BRIGHT_WAKELOCK_DURATION = 1000 * 30; // 30 minutes
+
+ private static WakeLock sScreenBrightWakeLock;
+
+ private AlertWakeLock() {}
+
+ public static void acquireScreenBrightWakeLock(Context context) {
+ acquireScreenBrightWakeLock(context, MAX_SCREEN_BRIGHT_WAKELOCK_DURATION);
+ }
+
+ public static void acquireScreenBrightWakeLock(Context context, long timeout) {
+ if (sScreenBrightWakeLock == null) {
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ sScreenBrightWakeLock = pm.newWakeLock(
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG);
+ }
+
+ if (!sScreenBrightWakeLock.isHeld()) {
+ sScreenBrightWakeLock.acquire(timeout);
+ Log.d(TAG, "acquired screen bright wakelock");
+ }
+ }
+
+ public static void releaseScreenBrightWakeLock() {
+ if (sScreenBrightWakeLock != null && sScreenBrightWakeLock.isHeld()) {
+ sScreenBrightWakeLock.release();
+ Log.d(TAG, "released screen bright wakelock");
+ }
+ }
+}
Index: packages/apps/Settings/res/values/strings.xml
===================================================================
--- packages/apps/Settings/res/values/strings.xml (版本 10656)
+++ packages/apps/Settings/res/values/strings.xml (版本 10657)
@@ -10171,4 +10171,8 @@
Ethernet
+ Reboot
+ Device will reboot.
log:Background execution not allowed: receiving XXX to XXX frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java
可以在阻止广播处添加ALARM_1和ALARM_2的广播。