package com.android.server.deviceconfig; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 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_REBOOT; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; 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.net.ConnectivityManager; import android.net.NetworkCapabilities; import android.util.Log; import androidx.test.filters.SmallTest; import java.time.ZoneId; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @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 = 1696496400000L; // 2023-10-05T02:00:00 private static final long RESCHEDULED_REBOOT_TIME = 1696582800000L; // 2023-10-06T02:00:00 private static final long OUTSIDE_WINDOW_REBOOT_TIME = 1696587000000L; // 2023-10-06T03:10:00 private static final long RESCHEDULED_OUTSIDE_WINDOW_REBOOT_TIME = 1696669200000L; // 2023-10-07T02:00:00 private static final long ELAPSED_REALTIME_1_DAY = 86400000L; private Context mContext; private KeyguardManager mKeyguardManager; private ConnectivityManager mConnectivityManager; private FakeInjector mFakeInjector; private UnattendedRebootManager mRebootManager; private SimPinReplayManager mSimPinReplayManager; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() throws Exception { mSetFlagsRule.enableFlags(FLAG_ENABLE_SIM_PIN_REPLAY); mSimPinReplayManager = mock(SimPinReplayManager.class); mKeyguardManager = mock(KeyguardManager.class); mConnectivityManager = mock(ConnectivityManager.class); 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; } return super.getSystemService(name); } }; mFakeInjector = new FakeInjector(); mRebootManager = new UnattendedRebootManager(mContext, mFakeInjector, mSimPinReplayManager); // 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); mFakeInjector.setElapsedRealtime(ELAPSED_REALTIME_1_DAY); } @Test public void scheduleReboot() { 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_noPinLock() { 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() { 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() { 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() { 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() { 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() { 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() { 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 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 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; return; } // Advance now to reboot time and reboot immediately. scheduledReboot = true; actualRebootTime = rebootTimeMillis; setNow(rebootTimeMillis); LatchingBroadcastReceiver rebootReceiver = new LatchingBroadcastReceiver(); // Wait for reboot broadcast to be sent. context.sendOrderedBroadcast( new Intent(ACTION_TRIGGER_REBOOT), null, rebootReceiver, null, 0, null, null); rebootReceiver.await(20, TimeUnit.SECONDS); } @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); } } } }