| 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 = 1; |
| 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= */ 0); |
| 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); |
| } |
| } |