summaryrefslogtreecommitdiff
path: root/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2024-01-18 00:27:05 -0800
committerXin Li <delphij@google.com>2024-01-18 00:27:05 -0800
commit1f8c0a519e8b885f4a89f603bf8e5d24289b2e54 (patch)
tree4cd02b4c6548bfcd4db3aafa09468612f31cbf20 /service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
parent91fb5d907f94a98829254bc43147c11dd700a588 (diff)
parentd0b716d0656320e9c2e3b0a690406f088b5124ac (diff)
downloadConfigInfrastructure-1f8c0a519e8b885f4a89f603bf8e5d24289b2e54.tar.gz
Merge Android 24Q1 Release (ab/11220357)temp_319669529
Bug: 319669529 Merged-In: I915dbb6b401285ad09fe1ed544b93bf9c2f43fe2 Change-Id: I6f4193a7a21630d874eb615c0c8ae5c295aa6227
Diffstat (limited to 'service/java/com/android/server/deviceconfig/UnattendedRebootManager.java')
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManager.java323
1 files changed, 323 insertions, 0 deletions
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
new file mode 100644
index 0000000..30f2439
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
@@ -0,0 +1,323 @@
+package com.android.server.deviceconfig;
+
+import static com.android.server.deviceconfig.Flags.enableSimPinReplay;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.os.SystemClock;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Reboot scheduler for applying aconfig flags.
+ *
+ * <p>If device is password protected, uses <a
+ * href="https://source.android.com/docs/core/ota/resume-on-reboot">Resume on Reboot</a> to reboot
+ * the device, otherwise proceeds with regular reboot.
+ *
+ * @hide
+ */
+final class UnattendedRebootManager {
+ private static final int DEFAULT_REBOOT_WINDOW_START_TIME_HOUR = 3;
+ private static final int DEFAULT_REBOOT_WINDOW_END_TIME_HOUR = 5;
+
+ private static final int DEFAULT_REBOOT_FREQUENCY_DAYS = 2;
+
+ private static final String TAG = "UnattendedRebootManager";
+
+ static final String REBOOT_REASON = "unattended,flaginfra";
+
+ @VisibleForTesting
+ static final String ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED =
+ "com.android.server.deviceconfig.RESUME_ON_REBOOOT_LSKF_CAPTURED";
+
+ @VisibleForTesting
+ static final String ACTION_TRIGGER_REBOOT = "com.android.server.deviceconfig.TRIGGER_REBOOT";
+
+ private final Context mContext;
+
+ private boolean mLskfCaptured;
+
+ private final UnattendedRebootManagerInjector mInjector;
+
+ private final SimPinReplayManager mSimPinReplayManager;
+
+ private static class InjectorImpl implements UnattendedRebootManagerInjector {
+ InjectorImpl() {
+ /*no op*/
+ }
+
+ public long now() {
+ return System.currentTimeMillis();
+ }
+
+ public ZoneId zoneId() {
+ return ZoneId.systemDefault();
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ public int getRebootStartTime() {
+ return DEFAULT_REBOOT_WINDOW_START_TIME_HOUR;
+ }
+
+ public int getRebootEndTime() {
+ return DEFAULT_REBOOT_WINDOW_END_TIME_HOUR;
+ }
+
+ public int getRebootFrequency() {
+ return DEFAULT_REBOOT_FREQUENCY_DAYS;
+ }
+
+ public void setRebootAlarm(Context context, long rebootTimeMillis) {
+ AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP, rebootTimeMillis, createTriggerRebootPendingIntent(context));
+ }
+
+ public void triggerRebootOnNetworkAvailable(Context context) {
+ final ConnectivityManager connectivityManager =
+ context.getSystemService(ConnectivityManager.class);
+ NetworkRequest request =
+ new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ connectivityManager.requestNetwork(request, createTriggerRebootPendingIntent(context));
+ }
+
+ public int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch)
+ throws IOException {
+ return RecoverySystem.rebootAndApply(context, reason, slotSwitch);
+ }
+
+ public void prepareForUnattendedUpdate(
+ @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender)
+ throws IOException {
+ RecoverySystem.prepareForUnattendedUpdate(context, updateToken, intentSender);
+ }
+
+ public boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException {
+ return RecoverySystem.isPreparedForUnattendedUpdate(context);
+ }
+
+ public void regularReboot(Context context) {
+ PowerManager powerManager = context.getSystemService(PowerManager.class);
+ powerManager.reboot(REBOOT_REASON);
+ }
+
+ private static PendingIntent createTriggerRebootPendingIntent(Context context) {
+ return PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 0,
+ new Intent(ACTION_TRIGGER_REBOOT),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+ }
+ }
+
+ @VisibleForTesting
+ UnattendedRebootManager(
+ Context context,
+ UnattendedRebootManagerInjector injector,
+ SimPinReplayManager simPinReplayManager) {
+ mContext = context;
+ mInjector = injector;
+ mSimPinReplayManager = simPinReplayManager;
+
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mLskfCaptured = true;
+ }
+ },
+ new IntentFilter(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED),
+ Context.RECEIVER_EXPORTED);
+
+ // Do not export receiver so that tests don't trigger reboot.
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ tryRebootOrSchedule();
+ }
+ },
+ new IntentFilter(ACTION_TRIGGER_REBOOT),
+ Context.RECEIVER_NOT_EXPORTED);
+ }
+
+ UnattendedRebootManager(Context context) {
+ this(context, new InjectorImpl(), new SimPinReplayManager(context));
+ }
+
+ public void prepareUnattendedReboot() {
+ Log.i(TAG, "Preparing for Unattended Reboot");
+ // RoR only supported on devices with screen lock.
+ if (!isDeviceSecure(mContext)) {
+ return;
+ }
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ mContext,
+ /* requestCode= */ 0,
+ new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+
+ try {
+ mInjector.prepareForUnattendedUpdate(
+ mContext, /* updateToken= */ "", pendingIntent.getIntentSender());
+ } catch (IOException e) {
+ Log.i(TAG, "prepareForUnattendedReboot failed with exception" + e.getLocalizedMessage());
+ }
+ }
+
+ public void scheduleReboot() {
+ // Reboot the next day at the reboot start time.
+ LocalDateTime timeToReboot =
+ Instant.ofEpochMilli(mInjector.now())
+ .atZone(mInjector.zoneId())
+ .toLocalDate()
+ .plusDays(mInjector.getRebootFrequency())
+ .atTime(mInjector.getRebootStartTime(), /* minute= */ 12);
+ long rebootTimeMillis = timeToReboot.atZone(mInjector.zoneId()).toInstant().toEpochMilli();
+ Log.v(TAG, "Scheduling unattended reboot at time " + timeToReboot);
+
+ if (timeToReboot.isBefore(
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(mInjector.now()), mInjector.zoneId()))) {
+ Log.w(TAG, "Reboot time has already passed.");
+ return;
+ }
+
+ mInjector.setRebootAlarm(mContext, rebootTimeMillis);
+ }
+
+ @VisibleForTesting
+ void tryRebootOrSchedule() {
+ Log.v(TAG, "Attempting unattended reboot");
+
+ // Has enough time passed since reboot?
+ if (TimeUnit.MILLISECONDS.toDays(mInjector.elapsedRealtime())
+ < mInjector.getRebootFrequency()) {
+ Log.v(
+ TAG,
+ "Device has already been rebooted in that last "
+ + mInjector.getRebootFrequency()
+ + " days.");
+ scheduleReboot();
+ return;
+ }
+ // Is RoR is supported?
+ if (!isDeviceSecure(mContext)) {
+ Log.v(TAG, "Device is not secure. Proceed with regular reboot");
+ mInjector.regularReboot(mContext);
+ return;
+ }
+ // Is RoR prepared?
+ if (!isPreparedForUnattendedReboot()) {
+ Log.v(TAG, "Lskf is not captured, reschedule reboot.");
+ prepareUnattendedReboot();
+ scheduleReboot();
+ return;
+ }
+ // Is network connected?
+ // TODO(b/305259443): Use after-boot network connectivity projection
+ if (!isNetworkConnected(mContext)) {
+ Log.i(TAG, "Network is not connected, reschedule reboot.");
+ mInjector.triggerRebootOnNetworkAvailable(mContext);
+ return;
+ }
+ // Is current time between reboot window?
+ int currentHour =
+ Instant.ofEpochMilli(mInjector.now())
+ .atZone(mInjector.zoneId())
+ .toLocalDateTime()
+ .getHour();
+ if (currentHour < mInjector.getRebootStartTime()
+ || currentHour >= mInjector.getRebootEndTime()) {
+ Log.v(TAG, "Reboot requested outside of reboot window, reschedule reboot.");
+ prepareUnattendedReboot();
+ scheduleReboot();
+ return;
+ }
+ // Is preparing for SIM PIN replay successful?
+ if (enableSimPinReplay() && !mSimPinReplayManager.prepareSimPinReplay()) {
+ Log.w(TAG, "Sim Pin Replay failed, reschedule reboot");
+ scheduleReboot();
+ }
+
+ // Proceed with RoR.
+ Log.v(TAG, "Rebooting device to apply device config flags.");
+ try {
+ int success = mInjector.rebootAndApply(mContext, REBOOT_REASON, /* slotSwitch= */ false);
+ if (success != 0) {
+ // If reboot is not successful, reschedule.
+ Log.w(TAG, "Unattended reboot failed, reschedule reboot.");
+ scheduleReboot();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ scheduleReboot();
+ }
+ }
+
+ private boolean isPreparedForUnattendedReboot() {
+ try {
+ boolean isPrepared = mInjector.isPreparedForUnattendedUpdate(mContext);
+ if (isPrepared != mLskfCaptured) {
+ Log.w(TAG, "isPrepared != mLskfCaptured. Received " + isPrepared);
+ }
+ return isPrepared;
+ } catch (IOException e) {
+ Log.w(TAG, e.getLocalizedMessage());
+ return mLskfCaptured;
+ }
+ }
+
+ /** Returns true if the device has screen lock. */
+ private static boolean isDeviceSecure(Context context) {
+ KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+ if (keyguardManager == null) {
+ // Unknown if device is locked, proceed with RoR anyway.
+ Log.w(TAG, "Keyguard manager is null, proceeding with RoR anyway.");
+ return true;
+ }
+ return keyguardManager.isDeviceSecure();
+ }
+
+ private static boolean isNetworkConnected(Context context) {
+ final ConnectivityManager connectivityManager =
+ context.getSystemService(ConnectivityManager.class);
+ if (connectivityManager == null) {
+ Log.w(TAG, "ConnectivityManager is null");
+ return false;
+ }
+ Network activeNetwork = connectivityManager.getActiveNetwork();
+ NetworkCapabilities networkCapabilities =
+ connectivityManager.getNetworkCapabilities(activeNetwork);
+ return networkCapabilities != null
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+ }
+}