Snap for 11413328 from e6a5a4a92145207e3b3c1a6cc3f96a18953dab88 to 24Q2-release

Change-Id: I7a6a7dd45897191762ea8361a3bbffe54c8276d9
diff --git a/service/ServiceResources/res/values/config.xml b/service/ServiceResources/res/values/config.xml
new file mode 100644
index 0000000..b116767
--- /dev/null
+++ b/service/ServiceResources/res/values/config.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2024, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds.  Do not translate.
+
+     NOTE: The naming convention is "config_camelCaseValue". Some legacy
+     entries do not follow the convention, but all new entries should. -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- The start (inclusive) and end (exclusive) hours for unattended reboot window.
+         To enforce the reboot window constraint, the values should be in [0, 23], and they should
+         not be equal to each other. Otherwise, a default window will be used.
+         If the start hour is bigger than the end hour, it will be treated as if the end hour is on
+         the next day. E.g. if start==22 and end==1, the reboot window will be from 10pm to 1am.
+         To remove reboot window constraints, set both `config_unattendedRebootStartHour`
+         and `config_unattendedRebootEndHour` to -1.-->
+    <integer name="config_unattendedRebootStartHour">3</integer>
+    <integer name="config_unattendedRebootEndHour">5</integer>
+
+    <!-- The unattended reboot frequency in days. Should be a positive value.
+         If a non-positive value is provided, the default frqeuency will be applied. -->
+    <integer name="config_unattendedRebootFrequencyDays">2</integer>
+
+    <!-- Whether or not the device needs to be charging to trigger unattended reboot.-->
+    <bool name="config_requireChargingForUnattendedReboot">false</bool>
+</resources>
\ No newline at end of file
diff --git a/service/flags.aconfig b/service/flags.aconfig
index e1396a0..f8a8d25 100644
--- a/service/flags.aconfig
+++ b/service/flags.aconfig
@@ -19,3 +19,17 @@
   description: "This flag controls enabling sim pin replay for unattended reboot."
   bug: "305269414"
 }
+
+flag {
+  name: "enable_custom_reboot_time_configurations"
+  namespace: "core_experiments_team_internal"
+  description: "This flags controls allowing devices to configure the reboot window and frequency."
+  bug: "322076175"
+}
+
+flag {
+  name: "enable_charger_dependency_for_reboot"
+  namespace: "core_experiments_team_internal"
+  description: "This flags controls allowing devices to configure reboot to require charging."
+  bug: "322076175"
+}
\ No newline at end of file
diff --git a/service/java/com/android/server/deviceconfig/RebootTimingConfiguration.java b/service/java/com/android/server/deviceconfig/RebootTimingConfiguration.java
new file mode 100644
index 0000000..0ff60f1
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/RebootTimingConfiguration.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.deviceconfig;
+
+import static com.android.server.deviceconfig.Flags.enableCustomRebootTimeConfigurations;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.deviceconfig.resources.R;
+
+import java.util.Optional;
+
+/**
+ * Contains the timing configuration for unattended reboot.
+ *
+ * @hide
+ */
+public class RebootTimingConfiguration {
+    private static final String RESOURCES_PACKAGE =
+        "com.android.server.deviceconfig.resources";
+
+    /**
+     * Special value that can be set for both the window start and end hour configs to disable
+     * reboot time window constraint.
+     */
+    private static final int ALLOW_ALL_HOURS = -1;
+
+    private static final Pair<Integer, Integer> DEFAULT_REBOOT_WINDOW_HOURS = Pair.create(3, 5);
+
+    private static final int DEFAULT_REBOOT_FREQUENCY_DAYS = 2;
+
+    private final Optional<Pair<Integer, Integer>> mRebootWindowStartEndHour;
+    private final int mRebootFrequencyDays;
+
+    public RebootTimingConfiguration(Context context) {
+        if (enableCustomRebootTimeConfigurations()) {
+            Context resourcesContext = getResourcesContext(context);
+            if (resourcesContext != null) {
+                Resources res = resourcesContext.getResources();
+                mRebootWindowStartEndHour = getRebootWindowStartEndHour(res);
+                mRebootFrequencyDays = getRebootFrequencyDays(res);
+                return;
+            }
+        }
+
+        mRebootWindowStartEndHour = Optional.of(DEFAULT_REBOOT_WINDOW_HOURS);
+        mRebootFrequencyDays = DEFAULT_REBOOT_FREQUENCY_DAYS;
+    }
+
+    @VisibleForTesting
+    RebootTimingConfiguration(
+            int rebootWindowStartHour, int rebootWindowEndHour, int rebootFrequencyDays) {
+        mRebootWindowStartEndHour =
+                getRebootWindowStartEndHour(rebootWindowStartHour, rebootWindowEndHour);
+        assert(isDayValid(rebootFrequencyDays));
+        mRebootFrequencyDays = rebootFrequencyDays;
+    }
+
+    /**
+     * Returns a {@link Pair} of integers, where the first element represents the reboot window
+     * start hour (inclusive), and the second element represents the reboot window end hour
+     * (exclusive). If the start hour is bigger than the end hour, it means that the end hour falls
+     * on the next day.
+     *
+     * <p>Returns an empty {@link Optional} if there is no reboot window constraint (i.e. if all
+     * hours are valid).
+     *
+     * <p>Use {@link #isHourWithinRebootHourWindow(int)} to validate if an hour falls within the
+     * window defined in this configuration.
+     */
+    public Optional<Pair<Integer, Integer>> getRebootWindowStartEndHour() {
+        return mRebootWindowStartEndHour;
+    }
+
+    /** Returns the reboot frequency in days. */
+    public int getRebootFrequencyDays() {
+        return mRebootFrequencyDays;
+    }
+
+    /**
+     * Returns {@code true} if the provided {@code hour} falls within the reboot window defined in
+     * this configuration.
+     */
+    public boolean isHourWithinRebootHourWindow(int hour) {
+        if (!isHourValid(hour)) {
+            return false;
+        }
+        if (mRebootWindowStartEndHour.isEmpty()) {
+            return true;
+        }
+        Pair<Integer, Integer> rebootWindowStartEndHour = mRebootWindowStartEndHour.get();
+        if (rebootWindowStartEndHour.first < rebootWindowStartEndHour.second) {
+            // Window lies in a single day.
+            return hour >= rebootWindowStartEndHour.first && hour < rebootWindowStartEndHour.second;
+        }
+        // Window ends on the next day.
+        return hour >= rebootWindowStartEndHour.first || hour < rebootWindowStartEndHour.second;
+    }
+
+    private static Optional<Pair<Integer, Integer>> getRebootWindowStartEndHour(Resources res) {
+        return getRebootWindowStartEndHour(
+                res.getInteger(R.integer.config_unattendedRebootStartHour),
+                res.getInteger(R.integer.config_unattendedRebootEndHour));
+    }
+
+    private static Optional<Pair<Integer, Integer>> getRebootWindowStartEndHour(
+            int configStartHour, int configEndHour) {
+        if (configStartHour == ALLOW_ALL_HOURS && configEndHour == ALLOW_ALL_HOURS) {
+            return Optional.empty();
+        }
+        if (!isHourValid(configStartHour)
+                || !isHourValid(configEndHour)
+                || configStartHour == configEndHour) {
+            return Optional.of(DEFAULT_REBOOT_WINDOW_HOURS);
+        }
+        return Optional.of(Pair.create(configStartHour, configEndHour));
+    }
+
+    private static int getRebootFrequencyDays(Resources res) {
+        int frequencyDays = res.getInteger(R.integer.config_unattendedRebootFrequencyDays);
+        if (!isDayValid(frequencyDays)) {
+            frequencyDays = DEFAULT_REBOOT_FREQUENCY_DAYS;
+        }
+        return frequencyDays;
+    }
+
+    private static boolean isHourValid(int hour) {
+        return hour >= 0 && hour <= 23;
+    }
+
+    private static boolean isHourWindowValid(int startHour, int endHour) {
+        return isHourValid(startHour) && isHourValid(endHour) && startHour != endHour;
+    }
+
+    private static boolean isDayValid(int day) {
+        return day > 0;
+    }
+
+    @Nullable
+    private static Context getResourcesContext(Context context) {
+        try {
+            return context.createPackageContext(RESOURCES_PACKAGE, 0);
+        } catch (NameNotFoundException e) {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
index 30f2439..4b7159a 100644
--- a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
@@ -1,5 +1,7 @@
 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;
@@ -16,16 +18,21 @@
 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;
 
 /**
@@ -56,12 +63,26 @@
 
   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*/
@@ -123,6 +144,11 @@
       return RecoverySystem.isPreparedForUnattendedUpdate(context);
     }
 
+    @Override
+    public boolean requiresChargingForReboot(Context context) {
+      return context.getResources().getBoolean(R.bool.config_requireChargingForUnattendedReboot);
+    }
+
     public void regularReboot(Context context) {
       PowerManager powerManager = context.getSystemService(PowerManager.class);
       powerManager.reboot(REBOOT_REASON);
@@ -141,10 +167,12 @@
   UnattendedRebootManager(
       Context context,
       UnattendedRebootManagerInjector injector,
-      SimPinReplayManager simPinReplayManager) {
+      SimPinReplayManager simPinReplayManager,
+      @Nullable RebootTimingConfiguration rebootTimingConfiguration) {
     mContext = context;
     mInjector = injector;
     mSimPinReplayManager = simPinReplayManager;
+    mRebootTimingConfiguration = rebootTimingConfiguration;
 
     mContext.registerReceiver(
         new BroadcastReceiver() {
@@ -169,7 +197,12 @@
   }
 
   UnattendedRebootManager(Context context) {
-    this(context, new InjectorImpl(), new SimPinReplayManager(context));
+    this(context,
+        new InjectorImpl(),
+        new SimPinReplayManager(context),
+        enableCustomRebootTimeConfigurations()
+            ? new RebootTimingConfiguration(context)
+            : null);
   }
 
   public void prepareUnattendedReboot() {
@@ -195,12 +228,20 @@
 
   public void scheduleReboot() {
     // Reboot the next day at the reboot start time.
+    final int rebootHour;
+    if (enableCustomRebootTimeConfigurations()) {
+      Optional<Pair<Integer, Integer>> 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(mInjector.getRebootFrequency())
-            .atTime(mInjector.getRebootStartTime(), /* minute= */ 12);
+            .plusDays(getRebootFrequencyDays())
+            .atTime(rebootHour, /* minute= */ 12);
     long rebootTimeMillis = timeToReboot.atZone(mInjector.zoneId()).toInstant().toEpochMilli();
     Log.v(TAG, "Scheduling unattended reboot at time " + timeToReboot);
 
@@ -217,14 +258,10 @@
   void tryRebootOrSchedule() {
     Log.v(TAG, "Attempting unattended reboot");
 
+    final int rebootFrequencyDays = getRebootFrequencyDays();
     // 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.");
+    if (TimeUnit.MILLISECONDS.toDays(mInjector.elapsedRealtime()) < rebootFrequencyDays) {
+      Log.v(TAG, "Device has already been rebooted in that last " + rebootFrequencyDays + " days.");
       scheduleReboot();
       return;
     }
@@ -254,8 +291,16 @@
             .atZone(mInjector.zoneId())
             .toLocalDateTime()
             .getHour();
-    if (currentHour < mInjector.getRebootStartTime()
-        || currentHour >= mInjector.getRebootEndTime()) {
+    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();
@@ -267,6 +312,13 @@
       scheduleReboot();
     }
 
+    if (enableChargerDependencyForReboot()
+        && mInjector.requiresChargingForReboot(mContext)
+        && !isCharging(mContext)) {
+      triggerRebootOnCharging();
+      return;
+    }
+
     // Proceed with RoR.
     Log.v(TAG, "Rebooting device to apply device config flags.");
     try {
@@ -282,6 +334,12 @@
     }
   }
 
+  private int getRebootFrequencyDays() {
+    return enableCustomRebootTimeConfigurations()
+        ? mRebootTimingConfiguration.getRebootFrequencyDays()
+        : mInjector.getRebootFrequency();
+  }
+
   private boolean isPreparedForUnattendedReboot() {
     try {
       boolean isPrepared = mInjector.isPreparedForUnattendedUpdate(mContext);
@@ -295,6 +353,16 @@
     }
   }
 
+  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);
@@ -306,6 +374,12 @@
     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);
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
index 5ca3e1e..f5f9850 100644
--- a/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
@@ -45,6 +45,8 @@
 
   boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException;
 
+  boolean requiresChargingForReboot(Context context);
+
   /** Regular reboot injector. */
   void regularReboot(Context context);
 }
diff --git a/service/javatests/Android.bp b/service/javatests/Android.bp
index 0177890..15f879a 100644
--- a/service/javatests/Android.bp
+++ b/service/javatests/Android.bp
@@ -52,6 +52,7 @@
         "android.test.runner",
         "framework-connectivity.stubs.module_lib",
         "framework-configinfrastructure",
+        "DeviceConfigServiceResources",
     ],
     // Test coverage system runs on different devices. Need to
     // compile for all architecture.
diff --git a/service/javatests/src/com/android/server/deviceconfig/RebootTimingConfigurationTest.java b/service/javatests/src/com/android/server/deviceconfig/RebootTimingConfigurationTest.java
new file mode 100644
index 0000000..e6691b0
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/RebootTimingConfigurationTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.deviceconfig;
+
+
+import static com.android.server.deviceconfig.Flags.FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.deviceconfig.resources.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Optional;
+
+@SmallTest
+public class RebootTimingConfigurationTest {
+
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+
+    @Before
+    public void setUp() throws Exception {
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS);
+        when(mContext.createPackageContext("com.android.server.deviceconfig.resources", 0))
+                .thenReturn(mContext);
+        when(mContext.getResources()).thenReturn(mResources);
+    }
+
+    @Test
+    public void validSameDayConfiguration() {
+        mockRebootWindowConfig(1, 3);
+        mockRebootFrequencyDays(4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.of(Pair.create(1, 3)));
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(4);
+        assertHoursWithinWindow(config, 1, 2);
+        assertHoursOutsideWindow(config, 0, 3, 4);
+    }
+
+    @Test
+    public void validNextDayConfiguration() {
+        mockRebootWindowConfig(22, 3);
+        mockRebootFrequencyDays(4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.of(Pair.create(22, 3)));
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(4);
+        assertHoursWithinWindow(config, 22, 23, 0, 1, 2);
+        assertHoursOutsideWindow(config, 20, 3, 4);
+    }
+
+    @Test
+    public void flagDisabled_usesDefaultWindowAndFrequency() {
+        mSetFlagsRule.disableFlags(FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS);
+        mockRebootWindowConfig(1, 9);
+        mockRebootFrequencyDays(4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.of(Pair.create(3, 5)));
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(2);
+    }
+
+    @Test
+    public void invalidWindowHoursWithEqualWindowTimes_usesDefaultWindow() {
+        mockRebootWindowConfig(1, 1);
+        mockRebootFrequencyDays(4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.of(Pair.create(3, 5)));
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(4);
+    }
+
+    @Test
+    public void invalidWindowHoursWithNegativeWindowTimes_usesDefaultWindow() {
+        mockRebootWindowConfig(-1, 1);
+        mockRebootFrequencyDays(4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.of(Pair.create(3, 5)));
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(4);
+    }
+
+    @Test
+    public void invalidFrequencyDay_usesDefaultFrequencyDay() {
+        mockRebootWindowConfig(2, 3);
+        mockRebootFrequencyDays(-4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.of(Pair.create(2, 3)));
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(2);
+    }
+
+    @Test
+    public void disableRebootWindowConfiguration() {
+        mockRebootWindowConfig(-1, -1);
+        mockRebootFrequencyDays(4);
+
+        RebootTimingConfiguration config = new RebootTimingConfiguration(mContext);
+
+        assertThat(config.getRebootWindowStartEndHour()).isEqualTo(Optional.empty());
+        assertThat(config.getRebootFrequencyDays()).isEqualTo(4);
+        for (int i = 0; i < 24; i++) {
+            assertHoursWithinWindow(config, i);
+        }
+    }
+
+    private void mockRebootWindowConfig(int windowStartHour, int windowEndHour) {
+        when(mResources.getInteger(R.integer.config_unattendedRebootStartHour))
+                .thenReturn(windowStartHour);
+        when(mResources.getInteger(R.integer.config_unattendedRebootEndHour))
+                .thenReturn(windowEndHour);
+    }
+
+    private void mockRebootFrequencyDays(int days) {
+        when(mResources.getInteger(R.integer.config_unattendedRebootFrequencyDays))
+                .thenReturn(days);
+    }
+
+    private void assertHoursWithinWindow(RebootTimingConfiguration config, int... hours) {
+        for (int hour : hours) {
+            assertThat(config.isHourWithinRebootHourWindow(hour)).isTrue();
+        }
+    }
+
+    private void assertHoursOutsideWindow(RebootTimingConfiguration config, int... hours) {
+        for (int hour : hours) {
+            assertThat(config.isHourWithinRebootHourWindow(hour)).isFalse();
+        }
+    }
+}
diff --git a/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
index 0ddbc64..93bd2b1 100644
--- a/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
+++ b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
@@ -1,6 +1,8 @@
 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;
@@ -9,7 +11,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 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;
@@ -22,18 +26,25 @@
 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;
 
 @SmallTest
 public class UnattendedRebootManagerTest {
@@ -52,9 +63,13 @@
           1696669920000L; // 2023-10-07T02:12:00
   private static final long ELAPSED_REALTIME_1_DAY = 86400000L;
 
+  private final List<BroadcastReceiverRegistration> 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;
@@ -65,11 +80,16 @@
 
   @Before
   public void setUp() throws Exception {
-    mSetFlagsRule.enableFlags(FLAG_ENABLE_SIM_PIN_REPLAY);
+    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()) {
@@ -79,13 +99,24 @@
               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);
+    mRebootManager =
+        new UnattendedRebootManager(
+            mContext, mFakeInjector, mSimPinReplayManager, mRebootTimingConfiguration);
 
     // Need to register receiver in tests so that the test doesn't trigger reboot requested by
     // deviceconfig.
@@ -100,6 +131,9 @@
         Context.RECEIVER_EXPORTED);
 
     mFakeInjector.setElapsedRealtime(ELAPSED_REALTIME_1_DAY);
+
+    mFakeInjector.setRequiresChargingForReboot(true);
+    when(mBatterManager.isCharging()).thenReturn(true);
   }
 
   @Test
@@ -123,6 +157,93 @@
   }
 
   @Test
+  public void scheduleReboot_requiresCharging_notCharging() {
+    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<BroadcastReceiverRegistration> 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() {
+    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() {
+    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() {
     Log.i(TAG, "scheduleReboot_noPinLock");
     when(mKeyguardManager.isDeviceSecure()).thenReturn(false);
@@ -218,7 +339,21 @@
   }
 
   @Test
-  public void scheduleReboot_elapsedRealtimeLessThanFrequency() {
+  public void scheduleReboot_elapsedRealtimeLessThanFrequency_withDefaultTimeConfigurations() {
+    scheduleReboot_elapsedRealtimeLessThanFrequency(/* enableCustomTimeConfig= */ false);
+  }
+
+  @Test
+  public void scheduleReboot_elapsedRealtimeLessThanFrequency_withCustomTimeConfigurations() {
+    scheduleReboot_elapsedRealtimeLessThanFrequency(/* enableCustomTimeConfig= */ true);
+  }
+
+  private void scheduleReboot_elapsedRealtimeLessThanFrequency(boolean enableCustomTimeConfig) {
+    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()))
@@ -239,7 +374,19 @@
   }
 
   @Test
-  public void tryRebootOrSchedule_outsideRebootWindow() {
+  public void tryRebootOrSchedule_outsideRebootWindow_withDefaultTimeConfigurations() {
+    tryRebootOrSchedule_outsideRebootWindow(/* enableCustomTimeConfig= */ false);
+  }
+
+  @Test
+  public void tryRebootOrSchedule_outsideRebootWindow_withCustomTimeConfigurations() {
+    tryRebootOrSchedule_outsideRebootWindow(/* enableCustomTimeConfig= */ true);
+  }
+
+  private void tryRebootOrSchedule_outsideRebootWindow(boolean enableCustomTimeConfig) {
+    if (enableCustomTimeConfig) {
+      mSetFlagsRule.enableFlags(FLAG_ENABLE_CUSTOM_REBOOT_TIME_CONFIGURATIONS);
+    }
     Log.i(TAG, "scheduleReboot_internetOutsideRebootWindow");
     when(mKeyguardManager.isDeviceSecure()).thenReturn(true);
     when(mConnectivityManager.getNetworkCapabilities(any()))
@@ -265,6 +412,7 @@
   static class FakeInjector implements UnattendedRebootManagerInjector {
 
     private boolean isPreparedForUnattendedReboot;
+    private boolean requiresChargingForReboot;
     private boolean rebootAndApplied;
     private boolean regularRebooted;
     private boolean requestedNetwork;
@@ -294,6 +442,15 @@
     }
 
     @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;
@@ -405,4 +562,24 @@
       }
     }
   }
+
+  private List<BroadcastReceiverRegistration> 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;
+    }
+  }
 }