android 定时重启设备

摘要:本文主要介绍一种定时重启设备的方案,目的是对于低内存设备,防止其由于长时间不关机且应用消耗过多RAM导致卡顿甚至无法开机的现象。基本方案是通过AlarmManager设置定时闹钟唤醒系统并且判断是否符合重启条件,从而进行弹框提醒和倒计时重启。

一、自动重启实现

由于是系统定制功能,所以设计之初定位在Settings模块中实现
patch代码说明:

1. 监听开机广播设置闹钟

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. 闹钟触发整点判断是否弹框

整点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);
+            }
+        }
+    }

3. 弹框逻辑

点击取消按钮,每隔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();
+    }

4. 循环间隔1小时闹钟触发

此触发很简单,直接弹框即可

+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d(TAG, "action = " + intent.getAction());
+
+        if (ALARM_2.equals(intent.getAction())) {
+            showRebootDialog(context);
+        }
+    }

5. 重启

重启接口很实用。

+    private void reboot() {
+        IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+        try {
+            pm.reboot(false, "reboot", true);
+        } catch (RemoteException e) {
+        }
+    }

6. 完整patch

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) {
+        }
+    }
+
+}

二、弹框倒计时

1. 方案及效果

此设计非常实用,分享给大家。灵感来源:android中警报框上的倒数计时器
设计理念就是在创建Dialog时设置setOnShowListener和setOnDismissListener,onShow获取重启按钮的文本并且创建倒计时CountDownTimer,在倒计时onTick方法中更新按钮的文本。

2. 完整patch

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) {
+        }
+    }
+}

三、自动亮屏

1. 方案

闹钟触发后只会唤醒系统,并不会亮屏,那Dialog自然也只有亮屏后才会显示,这就需要唤醒系统时亮屏显示Dialog。
添加亮屏唤醒锁即可。

2. 完整patch

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");
+        }
+    }
+}

四、其他

1. 资源

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.
 

2. 后台广播权限

log:Background execution not allowed: receiving XXX to XXX frameworks/base/services/core/java/com/android/server/am/BroadcastQueue.java
可以在阻止广播处添加ALARM_1和ALARM_2的广播。

你可能感兴趣的:(android_system,android)