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;
+ }
+ }
}