package com.android.server.deviceconfig; import static com.android.server.deviceconfig.Flags.enableChargerDependencyForReboot; import static com.android.server.deviceconfig.Flags.enableCustomRebootTimeConfigurations; 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.content.pm.PackageManager.NameNotFoundException; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.os.BatteryManager; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.SystemClock; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.server.deviceconfig.resources.R; import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Optional; 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; // Same as time RoR token is valid for. private static final int DEFAULT_PREPARATION_FALLBACK_DELAY_MINUTES = 10; 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"; @VisibleForTesting static final String ACTION_TRIGGER_PREPARATION_FALLBACK = "com.android.server.deviceconfig.TRIGGER_PREPERATION_FALLBACK"; private final Context mContext; @Nullable private final RebootTimingConfiguration mRebootTimingConfiguration; private boolean mLskfCaptured; private final UnattendedRebootManagerInjector mInjector; private final SimPinReplayManager mSimPinReplayManager; private boolean mChargingReceiverRegistered; private final BroadcastReceiver mChargingReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mChargingReceiverRegistered = false; mContext.unregisterReceiver(mChargingReceiver); tryRebootOrSchedule(); } }; 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, createTriggerActionPendingIntent(context, ACTION_TRIGGER_REBOOT)); } @Override public void setPrepareForUnattendedRebootFallbackAlarm(Context context, long delayMillis) { long alarmTime = now() + delayMillis; AlarmManager alarmManager = context.getSystemService(AlarmManager.class); alarmManager.set( AlarmManager.RTC_WAKEUP, alarmTime, createTriggerActionPendingIntent(context, ACTION_TRIGGER_PREPARATION_FALLBACK)); } @Override public void cancelPrepareForUnattendedRebootFallbackAlarm(Context context) { AlarmManager alarmManager = context.getSystemService(AlarmManager.class); alarmManager.cancel( createTriggerActionPendingIntent(context, ACTION_TRIGGER_PREPARATION_FALLBACK)); } 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, createTriggerActionPendingIntent(context, ACTION_TRIGGER_REBOOT)); } 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); } @Override public boolean requiresChargingForReboot(Context context) { ServiceResourcesHelper resourcesHelper = ServiceResourcesHelper.get(context); Optional resourcesPackageName = resourcesHelper.getResourcesPackageName(); if (!resourcesPackageName.isPresent()) { Log.w(TAG, "requiresChargingForReboot: unable to find resources package name"); return false; } Context resourcesContext; try { resourcesContext = context.createPackageContext(resourcesPackageName.get(), 0); } catch (NameNotFoundException e) { Log.e(TAG, "requiresChargingForReboot: Error in creating resources package context.", e); return false; } if (resourcesContext == null) { Log.w(TAG, "requiresChargingForReboot: unable to create resources context"); return false; } return resourcesContext .getResources() .getBoolean(R.bool.config_requireChargingForUnattendedReboot); } public void regularReboot(Context context) { PowerManager powerManager = context.getSystemService(PowerManager.class); powerManager.reboot(REBOOT_REASON); } private static PendingIntent createTriggerActionPendingIntent(Context context, String action) { return PendingIntent.getBroadcast( context, /* requestCode= */ 0, new Intent(action), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); } } @VisibleForTesting UnattendedRebootManager( Context context, UnattendedRebootManagerInjector injector, SimPinReplayManager simPinReplayManager, @Nullable RebootTimingConfiguration rebootTimingConfiguration) { mContext = context; mInjector = injector; mSimPinReplayManager = simPinReplayManager; mRebootTimingConfiguration = rebootTimingConfiguration; 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); mContext.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { prepareUnattendedReboot(); } }, new IntentFilter(ACTION_TRIGGER_PREPARATION_FALLBACK), Context.RECEIVER_NOT_EXPORTED); } UnattendedRebootManager(Context context) { this( context, new InjectorImpl(), new SimPinReplayManager(context), enableCustomRebootTimeConfigurations() ? new RebootTimingConfiguration(context) : null); } public void maybePrepareUnattendedReboot() { Log.d(TAG, "Setting timeout for preparing unattended reboot."); // RoR only supported on devices with screen lock. if (!isDeviceSecure(mContext)) { return; } // In the case of RoR failure or reboot without RoR, the device will stay in // LOCKED_BOOT_COMPLETED state until primary auth. // Since preparing for RoR can clear RoR state, wait sufficient time for RoR to finish // before sending fallback preparation during LOCKED_BOOT_STATE. mInjector.setPrepareForUnattendedRebootFallbackAlarm( mContext, TimeUnit.MINUTES.toMillis(DEFAULT_PREPARATION_FALLBACK_DELAY_MINUTES)); } public void prepareUnattendedReboot() { Log.i(TAG, "Preparing for Unattended Reboot"); // RoR only supported on devices with screen lock. if (!isDeviceSecure(mContext)) { return; } if (isPreparedForUnattendedReboot()) { Log.d(TAG, "Unattended reboot has already been prepared, skip"); 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()); } mInjector.cancelPrepareForUnattendedRebootFallbackAlarm(mContext); } public void scheduleReboot() { // Reboot the next day at the reboot start time. final int rebootHour; if (enableCustomRebootTimeConfigurations()) { Optional> rebootWindowStartEndHour = mRebootTimingConfiguration.getRebootWindowStartEndHour(); rebootHour = rebootWindowStartEndHour.isEmpty() ? 0 : rebootWindowStartEndHour.get().first; } else { rebootHour = mInjector.getRebootStartTime(); } LocalDateTime timeToReboot = Instant.ofEpochMilli(mInjector.now()) .atZone(mInjector.zoneId()) .toLocalDate() .plusDays(getRebootFrequencyDays()) .atTime(rebootHour, /* 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"); final int rebootFrequencyDays = getRebootFrequencyDays(); // Has enough time passed since reboot? if (TimeUnit.MILLISECONDS.toDays(mInjector.elapsedRealtime()) < rebootFrequencyDays) { Log.v(TAG, "Device has already been rebooted in that last " + rebootFrequencyDays + " 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(); final boolean isHourWithinRebootHourWindow; if (enableCustomRebootTimeConfigurations()) { isHourWithinRebootHourWindow = mRebootTimingConfiguration.isHourWithinRebootHourWindow(currentHour); } else { isHourWithinRebootHourWindow = currentHour >= mInjector.getRebootStartTime() && currentHour < mInjector.getRebootEndTime(); } if (!isHourWithinRebootHourWindow) { 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(); } if (enableChargerDependencyForReboot() && mInjector.requiresChargingForReboot(mContext) && !isCharging(mContext)) { triggerRebootOnCharging(); return; } // 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 int getRebootFrequencyDays() { return enableCustomRebootTimeConfigurations() ? mRebootTimingConfiguration.getRebootFrequencyDays() : mInjector.getRebootFrequency(); } 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; } } private void triggerRebootOnCharging() { if (!mChargingReceiverRegistered) { mChargingReceiverRegistered = true; mContext.registerReceiver( mChargingReceiver, new IntentFilter(BatteryManager.ACTION_CHARGING), Context.RECEIVER_EXPORTED); } } /** 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 isCharging(Context context) { BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); return batteryManager.isCharging(); } 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); } }