package com.android.server.deviceconfig; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.server.deviceconfig.Flags.FLAG_ENABLE_CHARGER_DEPENDENCY_FOR_REBOOT; import static com.android.server.deviceconfig.Flags.FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS; import static com.android.server.deviceconfig.Flags.FLAG_ENABLE_SIM_PIN_REPLAY; import static com.android.server.deviceconfig.UnattendedRebootManager.ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED; import static com.android.server.deviceconfig.UnattendedRebootManager.ACTION_TRIGGER_PREPARATION_FALLBACK; import static com.android.server.deviceconfig.UnattendedRebootManager.ACTION_TRIGGER_REBOOT; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.platform.test.flag.junit.SetFlagsRule; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.app.KeyguardManager; import android.content.BroadcastReceiver; import android.content.ContextWrapper; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.NetworkCapabilities; import android.os.BatteryManager; import android.util.Log; import androidx.test.filters.SmallTest; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import com.android.modules.utils.build.SdkLevel; @SmallTest public class UnattendedRebootManagerTest { private static final String TAG = "UnattendedRebootManagerTest"; private static final int REBOOT_FREQUENCY = 1; private static final int REBOOT_START_HOUR = 2; private static final int REBOOT_END_HOUR = 3; private static final long CURRENT_TIME = 1696452549304L; // 2023-10-04T13:49:09.304 private static final long REBOOT_TIME = 1696497120000L; // 2023-10-05T02:12:00 private static final long RESCHEDULED_REBOOT_TIME = 1696583520000L; // 2023-10-06T02:12:00 private static final long OUTSIDE_WINDOW_REBOOT_TIME = 1696587000000L; // 2023-10-06T03:10:00 private static final long RESCHEDULED_OUTSIDE_WINDOW_REBOOT_TIME = 1696669920000L; // 2023-10-07T02:12:00 private static final long ELAPSED_REALTIME_1_DAY = 86400000L; private final List mRegisteredReceivers = new ArrayList<>(); private Context mContext; private BatteryManager mBatterManager; private KeyguardManager mKeyguardManager; private ConnectivityManager mConnectivityManager; private RebootTimingConfiguration mRebootTimingConfiguration; private FakeInjector mFakeInjector; private UnattendedRebootManager mRebootManager; private SimPinReplayManager mSimPinReplayManager; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() throws Exception { assumeTrue(SdkLevel.isAtLeastV()); mSetFlagsRule.enableFlags( FLAG_ENABLE_SIM_PIN_REPLAY, FLAG_ENABLE_CHARGER_DEPENDENCY_FOR_REBOOT); mSimPinReplayManager = mock(SimPinReplayManager.class); mKeyguardManager = mock(KeyguardManager.class); mConnectivityManager = mock(ConnectivityManager.class); mBatterManager = mock(BatteryManager.class); mRebootTimingConfiguration = new RebootTimingConfiguration(REBOOT_START_HOUR, REBOOT_END_HOUR, REBOOT_FREQUENCY); mContext = new ContextWrapper(getInstrumentation().getTargetContext()) { @Override public Object getSystemService(String name) { if (name.equals(Context.KEYGUARD_SERVICE)) { return mKeyguardManager; } else if (name.equals(Context.CONNECTIVITY_SERVICE)) { return mConnectivityManager; } else if (name.equals(Context.BATTERY_SERVICE)) { return mBatterManager; } return super.getSystemService(name); } @Override public Intent registerReceiver( @Nullable BroadcastReceiver receiver, IntentFilter filter, int flags) { mRegisteredReceivers.add(new BroadcastReceiverRegistration(receiver, filter, flags)); return super.registerReceiver(receiver, filter, flags); } }; mFakeInjector = new FakeInjector(); mRebootManager = new UnattendedRebootManager( mContext, mFakeInjector, mSimPinReplayManager, mRebootTimingConfiguration); // Need to register receiver in tests so that the test doesn't trigger reboot requested by // deviceconfig. mContext.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mRebootManager.tryRebootOrSchedule(); } }, new IntentFilter(ACTION_TRIGGER_REBOOT), Context.RECEIVER_EXPORTED); mContext.registerReceiver( new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mRebootManager.prepareUnattendedReboot(); } }, new IntentFilter(ACTION_TRIGGER_PREPARATION_FALLBACK), Context.RECEIVER_EXPORTED); mFakeInjector.setElapsedRealtime(ELAPSED_REALTIME_1_DAY); mFakeInjector.setRequiresChargingForReboot(true); when(mBatterManager.isCharging()).thenReturn(true); } @Test public void maybePrepareUnattendedReboot() { assumeTrue(SdkLevel.isAtLeastV()); // After normal flow Log.i(TAG, "maybePrepareUnattendedReboot"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); mRebootManager.maybePrepareUnattendedReboot(); assertTrue(mFakeInjector.isPreparedForUnattendedUpdate(mContext)); } @Test public void scheduleReboot() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); assertTrue(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); } @Test public void scheduleReboot_requiresCharging_notCharging() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_requiresCharging_notCharging"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mSetFlagsRule.enableFlags(FLAG_ENABLE_CHARGER_DEPENDENCY_FOR_REBOOT); mFakeInjector.setRequiresChargingForReboot(true); when(mBatterManager.isCharging()).thenReturn(false); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); // Charging is required and device is not charging, so reboot should not be triggered. assertFalse(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); List chargingStateReceiverRegistrations = getRegistrationsForAction(BatteryManager.ACTION_CHARGING); assertThat(chargingStateReceiverRegistrations).hasSize(1); // Now mimic a change in a charging state changed, and verify that we do the reboot once // device // is charging. when(mBatterManager.isCharging()).thenReturn(true); BroadcastReceiver chargingStateReceiver = chargingStateReceiverRegistrations.get(0).mReceiver; chargingStateReceiver.onReceive(mContext, new Intent(BatteryManager.ACTION_CHARGING)); assertTrue(mFakeInjector.isRebootAndApplied()); } @Test public void scheduleReboot_doesNotRequireCharging_notCharging() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_doesNotRequireCharging_notCharging"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mSetFlagsRule.enableFlags(FLAG_ENABLE_CHARGER_DEPENDENCY_FOR_REBOOT); mFakeInjector.setRequiresChargingForReboot(false); when(mBatterManager.isCharging()).thenReturn(false); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); // Charging is not required, so reboot should be triggered despite the fact that the device // is not charging. assertTrue(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); assertThat(getRegistrationsForAction(BatteryManager.ACTION_CHARGING)).isEmpty(); } @Test public void scheduleReboot_requiresCharging_flagNotEnabled() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_requiresCharging_flagNotEnabled"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mSetFlagsRule.disableFlags(FLAG_ENABLE_CHARGER_DEPENDENCY_FOR_REBOOT); mFakeInjector.setRequiresChargingForReboot(true); when(mBatterManager.isCharging()).thenReturn(false); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); // Charging is required, but the flag that controls the feature to depend on charging is not // enabled, so eboot should be triggered despite the fact that the device is not charging. assertTrue(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); assertThat(getRegistrationsForAction(BatteryManager.ACTION_CHARGING)).isEmpty(); } @Test public void scheduleReboot_noPinLock() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_noPinLock"); when(mKeyguardManager.isDeviceSecure()).thenReturn(false); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); assertFalse(mFakeInjector.isRebootAndApplied()); assertTrue(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); } @Test public void scheduleReboot_noPreparation() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_noPreparation"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mRebootManager.scheduleReboot(); assertFalse(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(RESCHEDULED_REBOOT_TIME); } @Test public void scheduleReboot_simPinPreparationFailed() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(false).thenReturn(true); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); assertTrue(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(RESCHEDULED_REBOOT_TIME); } @Test public void scheduleReboot_noInternet() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_noInternet"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(new NetworkCapabilities()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); assertFalse(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); assertTrue(mFakeInjector.isRequestedNetwork()); } @Test public void scheduleReboot_noInternetValidation() { assumeTrue(SdkLevel.isAtLeastV()); Log.i(TAG, "scheduleReboot_noInternetValidation"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); assertFalse(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME); assertTrue(mFakeInjector.isRequestedNetwork()); } @Test public void scheduleReboot_elapsedRealtimeLessThanFrequency_withDefaultTimeConfigurations() { assumeTrue(SdkLevel.isAtLeastV()); scheduleReboot_elapsedRealtimeLessThanFrequency(/* enableCustomTimeConfig= */ false); } @Test public void scheduleReboot_elapsedRealtimeLessThanFrequency_withCustomTimeConfigurations() { assumeTrue(SdkLevel.isAtLeastV()); scheduleReboot_elapsedRealtimeLessThanFrequency(/* enableCustomTimeConfig= */ true); } private void scheduleReboot_elapsedRealtimeLessThanFrequency(boolean enableCustomTimeConfig) { assumeTrue(SdkLevel.isAtLeastV()); if (enableCustomTimeConfig) { mSetFlagsRule.enableFlags(FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS); } else { mSetFlagsRule.disableFlags(FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS); } Log.i(TAG, "scheduleReboot_elapsedRealtimeLessThanFrequency"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mFakeInjector.setElapsedRealtime(82800000); // 23 hours mRebootManager.prepareUnattendedReboot(); mRebootManager.scheduleReboot(); assertFalse(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(RESCHEDULED_REBOOT_TIME); } @Test public void tryRebootOrSchedule_outsideRebootWindow_withDefaultTimeConfigurations() { assumeTrue(SdkLevel.isAtLeastV()); tryRebootOrSchedule_outsideRebootWindow(/* enableCustomTimeConfig= */ false); } @Test public void tryRebootOrSchedule_outsideRebootWindow_withCustomTimeConfigurations() { assumeTrue(SdkLevel.isAtLeastV()); tryRebootOrSchedule_outsideRebootWindow(/* enableCustomTimeConfig= */ true); } private void tryRebootOrSchedule_outsideRebootWindow(boolean enableCustomTimeConfig) { assumeTrue(SdkLevel.isAtLeastV()); if (enableCustomTimeConfig) { mSetFlagsRule.enableFlags(FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS); } Log.i(TAG, "scheduleReboot_internetOutsideRebootWindow"); when(mKeyguardManager.isDeviceSecure()).thenReturn(true); when(mConnectivityManager.getNetworkCapabilities(any())) .thenReturn( new NetworkCapabilities.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) .build()); when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true); mFakeInjector.setNow(OUTSIDE_WINDOW_REBOOT_TIME); mRebootManager.prepareUnattendedReboot(); // Simulating case when reboot is tried after network connection is established outside the // reboot window. mRebootManager.tryRebootOrSchedule(); assertTrue(mFakeInjector.isRebootAndApplied()); assertFalse(mFakeInjector.isRegularRebooted()); assertThat(mFakeInjector.getActualRebootTime()) .isEqualTo(RESCHEDULED_OUTSIDE_WINDOW_REBOOT_TIME); } static class FakeInjector implements UnattendedRebootManagerInjector { private boolean isPreparedForUnattendedReboot; private boolean requiresChargingForReboot; private boolean rebootAndApplied; private boolean regularRebooted; private boolean requestedNetwork; private long actualRebootTime; private boolean scheduledReboot; private long nowMillis; private long elapsedRealtimeMillis; FakeInjector() { nowMillis = CURRENT_TIME; } @Override public void prepareForUnattendedUpdate( @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender) { context.sendBroadcast(new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED)); isPreparedForUnattendedReboot = true; } @Override public boolean isPreparedForUnattendedUpdate(@NonNull Context context) { return isPreparedForUnattendedReboot; } @Override public boolean requiresChargingForReboot(Context context) { return requiresChargingForReboot; } void setRequiresChargingForReboot(boolean requiresCharging) { requiresChargingForReboot = requiresCharging; } @Override public int rebootAndApply( @NonNull Context context, @NonNull String reason, boolean slotSwitch) { rebootAndApplied = true; return 0; // No error. } @Override public int getRebootFrequency() { return REBOOT_FREQUENCY; } @Override public void setRebootAlarm(Context context, long rebootTimeMillis) { // To prevent infinite loop, do not simulate another reboot if reboot was already // scheduled. if (scheduledReboot) { actualRebootTime = rebootTimeMillis; actualRebootTime = rebootTimeMillis; return; } // Advance now to reboot time and reboot immediately. scheduledReboot = true; actualRebootTime = rebootTimeMillis; triggerAlarmImmediately(context, rebootTimeMillis, ACTION_TRIGGER_REBOOT); } private void triggerAlarmImmediately(Context context, long time, String intent) { setNow(time); LatchingBroadcastReceiver rebootReceiver = new LatchingBroadcastReceiver(); // Wait for reboot broadcast to be sent. context.sendOrderedBroadcast(new Intent(intent), null, rebootReceiver, null, 0, null, null); rebootReceiver.await(20, TimeUnit.SECONDS); } @Override public void setPrepareForUnattendedRebootFallbackAlarm(Context context, long delayMillis) { triggerAlarmImmediately(context, delayMillis, ACTION_TRIGGER_PREPARATION_FALLBACK); } @Override public void cancelPrepareForUnattendedRebootFallbackAlarm(Context context) { /*no op */ } @Override public void triggerRebootOnNetworkAvailable(Context context) { requestedNetwork = true; } public boolean isRequestedNetwork() { return requestedNetwork; } @Override public int getRebootStartTime() { return REBOOT_START_HOUR; } @Override public int getRebootEndTime() { return REBOOT_END_HOUR; } @Override public long now() { return nowMillis; } public void setNow(long nowMillis) { this.nowMillis = nowMillis; } @Override public ZoneId zoneId() { return ZoneId.of("America/Los_Angeles"); } @Override public long elapsedRealtime() { return elapsedRealtimeMillis; } public void setElapsedRealtime(long elapsedRealtimeMillis) { this.elapsedRealtimeMillis = elapsedRealtimeMillis; } @Override public void regularReboot(Context context) { regularRebooted = true; } boolean isRebootAndApplied() { return rebootAndApplied; } boolean isRegularRebooted() { return regularRebooted; } public long getActualRebootTime() { return actualRebootTime; } } /** * A {@link BroadcastReceiver} with an internal latch that unblocks once any intent is received. */ private static class LatchingBroadcastReceiver extends BroadcastReceiver { private CountDownLatch latch = new CountDownLatch(1); @Override public void onReceive(Context context, Intent intent) { latch.countDown(); } public boolean await(long timeoutInMs, TimeUnit timeUnit) { try { return latch.await(timeoutInMs, timeUnit); } catch (InterruptedException e) { throw new RuntimeException(e); } } } private List getRegistrationsForAction(String action) { return mRegisteredReceivers.stream() .filter(r -> r.mFilter.hasAction(action)) .collect(Collectors.toList()); } /** Data class to store BroadcastReceiver registration info. */ private static final class BroadcastReceiverRegistration { final BroadcastReceiver mReceiver; final IntentFilter mFilter; final int mFlags; BroadcastReceiverRegistration(BroadcastReceiver receiver, IntentFilter filter, int flags) { mReceiver = receiver; mFilter = filter; mFlags = flags; } } }