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. * *

If device is password protected, uses Resume on Reboot 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); } }