Check for network connectivity before rebooting device, rescheduling if is none.

Under the current implementation, we will not reschedule reboot if there
is no network connectivity.

Under new implementation, we try reboot once internet connectivity is
established, scheduling for the following time if the current time is
outside the reboot window. The current reboot window is between 1-5.

Bug: 305259443
Test: atest ConfigInfrastructureServiceUnitTests[com.google.android.configinfrastructure.apex]
Change-Id: Ifaa9c80bb12472515b439c84d5f681d2a290a4ca
diff --git a/service/Android.bp b/service/Android.bp
index 8d0fb79..71417cf 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -38,6 +38,7 @@
     libs: [
         "framework-configinfrastructure.impl",
         "DeviceConfigServiceResources",
+        "framework-connectivity.stubs.module_lib",
     ],
     min_sdk_version: "UpsideDownCake",
     sdk_version: "system_server_current",
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
index c360f4d..655ad54 100644
--- a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
@@ -10,6 +10,10 @@
 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.util.Log;
@@ -29,7 +33,8 @@
  * @hide
  */
 final class UnattendedRebootManager {
-  private static final int DEFAULT_REBOOT_WINDOW_START_TIME_HOUR = 2;
+  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;
 
@@ -67,20 +72,28 @@
       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);
-      PendingIntent pendingIntent =
-          PendingIntent.getBroadcast(
-              context,
-              /* requestCode= */ 0,
-              new Intent(ACTION_TRIGGER_REBOOT),
-              PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+      alarmManager.setExact(
+          AlarmManager.RTC_WAKEUP, rebootTimeMillis, createTriggerRebootPendingIntent(context));
+    }
 
-      alarmManager.setExact(AlarmManager.RTC_WAKEUP, rebootTimeMillis, pendingIntent);
+    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)
@@ -102,6 +115,14 @@
       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
@@ -178,22 +199,53 @@
 
   @VisibleForTesting
   void tryRebootOrSchedule() {
-    // TODO(b/305259443): check network is connected
-    // Check if RoR is supported.
+    Log.v(TAG, "Attempting unattended reboot");
+
+    // Is RoR is supported?
     if (!isDeviceSecure(mContext)) {
       Log.v(TAG, "Device is not secure. Proceed with regular reboot");
       mInjector.regularReboot(mContext);
-    } else if (isPreparedForUnattendedReboot()) {
-      try {
-        mInjector.rebootAndApply(mContext, REBOOT_REASON, /* slotSwitch= */ false);
-      } catch (IOException e) {
-        Log.e(TAG, e.getLocalizedMessage());
-      }
-      // If reboot is successful, should not reach this.
-    } else {
-      // Lskf is not captured, try again the following day
+      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, schedule reboot for another time.");
+      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.");
+      prepareUnattendedReboot();
+      scheduleReboot();
+      return;
+    }
+
+    // Proceed with RoR.
+    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.");
+        scheduleReboot();
+      }
+    } catch (IOException e) {
+      Log.e(TAG, e.getLocalizedMessage());
+      scheduleReboot();
     }
   }
 
@@ -220,4 +272,19 @@
     }
     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);
+  }
 }
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
index 4e883d3..e1e0909 100644
--- a/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
@@ -23,11 +23,16 @@
   /** Reboot time injectors. */
   int getRebootStartTime();
 
+  int getRebootEndTime();
+
   int getRebootFrequency();
 
   /** Reboot Alarm injector. */
   void setRebootAlarm(Context context, long rebootTimeMillis);
 
+  /** Connectivity injector. */
+  void triggerRebootOnNetworkAvailable(Context context);
+
   /** {@link RecoverySystem} methods injectors. */
   int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch)
       throws IOException;
diff --git a/service/javatests/Android.bp b/service/javatests/Android.bp
index eb1561c..d9617d8 100644
--- a/service/javatests/Android.bp
+++ b/service/javatests/Android.bp
@@ -49,6 +49,7 @@
         "android.test.base",
         "android.test.mock",
         "android.test.runner",
+        "framework-connectivity.stubs.module_lib",
         "framework-configinfrastructure",
     ],
     // Test coverage system runs on different devices. Need to
diff --git a/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
index f87cf56..20494e5 100644
--- a/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
+++ b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
@@ -6,6 +6,7 @@
 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;
 
@@ -18,10 +19,11 @@
 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.io.IOException;
 import java.time.ZoneId;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -32,22 +34,27 @@
 @SmallTest
 public class UnattendedRebootManagerTest {
 
+  private static final String TAG = "UnattendedRebootManagerTest";
+
   private static final int REBOOT_FREQUENCY = 1;
-  private static final int REBOOT_HOUR = 2;
+  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 = 1696583400000L; // 2023-10-06T03:10:00
 
   private Context mContext;
-
   private KeyguardManager mKeyguardManager;
-
-  FakeInjector mFakeInjector;
-
+  private ConnectivityManager mConnectivityManager;
+  private FakeInjector mFakeInjector;
   private UnattendedRebootManager mRebootManager;
 
   @Before
   public void setUp() throws Exception {
     mKeyguardManager = mock(KeyguardManager.class);
+    mConnectivityManager = mock(ConnectivityManager.class);
 
     mContext =
         new ContextWrapper(getInstrumentation().getTargetContext()) {
@@ -55,6 +62,8 @@
           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);
           }
@@ -78,35 +87,111 @@
 
   @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());
 
     mRebootManager.prepareUnattendedReboot();
     mRebootManager.scheduleReboot();
 
-    assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
     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());
 
     mRebootManager.prepareUnattendedReboot();
     mRebootManager.scheduleReboot();
 
-    assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
     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());
 
     mRebootManager.scheduleReboot();
 
+    assertFalse(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());
+
+    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());
+
+    mRebootManager.prepareUnattendedReboot();
+    mRebootManager.scheduleReboot();
+
+    assertFalse(mFakeInjector.isRebootAndApplied());
+    assertFalse(mFakeInjector.isRegularRebooted());
+    assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+    assertTrue(mFakeInjector.isRequestedNetwork());
+  }
+
+  @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());
+    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();
+
     assertFalse(mFakeInjector.isRebootAndApplied());
     assertFalse(mFakeInjector.isRegularRebooted());
   }
@@ -116,27 +201,33 @@
     private boolean isPreparedForUnattendedReboot;
     private boolean rebootAndApplied;
     private boolean regularRebooted;
+    private boolean requestedNetwork;
     private long actualRebootTime;
+    private boolean scheduledReboot;
 
-    FakeInjector() {}
+    private long nowMillis;
+
+    FakeInjector() {
+      nowMillis = CURRENT_TIME;
+    }
 
     @Override
     public void prepareForUnattendedUpdate(
-        @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender)
-        throws IOException {
+        @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) throws IOException {
+    public boolean isPreparedForUnattendedUpdate(@NonNull Context context) {
       return isPreparedForUnattendedReboot;
     }
 
     @Override
     public int rebootAndApply(
         @NonNull Context context, @NonNull String reason, boolean slotSwitch) {
-      Log.i("UnattendedRebootManagerTest", "MockInjector.rebootAndApply");
       rebootAndApplied = true;
       return 0; // No error.
     }
@@ -148,24 +239,51 @@
 
     @Override
     public void setRebootAlarm(Context context, long rebootTimeMillis) {
-      // reboot immediately
+      // 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;
-      context.sendBroadcast(new Intent(UnattendedRebootManager.ACTION_TRIGGER_REBOOT));
+      setNow(rebootTimeMillis);
 
       LatchingBroadcastReceiver rebootReceiver = new LatchingBroadcastReceiver();
-      context.registerReceiver(
-          rebootReceiver, new IntentFilter(ACTION_TRIGGER_REBOOT), Context.RECEIVER_EXPORTED);
-      rebootReceiver.await(10, TimeUnit.SECONDS);
+
+      // 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_HOUR;
+      return REBOOT_START_HOUR;
+    }
+
+    @Override
+    public int getRebootEndTime() {
+      return REBOOT_END_HOUR;
     }
 
     @Override
     public long now() {
-      return CURRENT_TIME;
+      return nowMillis;
+    }
+
+    public void setNow(long nowMillis) {
+      this.nowMillis = nowMillis;
     }
 
     @Override
@@ -175,7 +293,6 @@
 
     @Override
     public void regularReboot(Context context) {
-      Log.i("UnattendedRebootManagerTest", "MockInjector.regularRebooted");
       regularRebooted = true;
     }
 
@@ -207,7 +324,7 @@
       try {
         return latch.await(timeoutInMs, timeUnit);
       } catch (InterruptedException e) {
-        return false;
+        throw new RuntimeException(e);
       }
     }
   }