summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYeabkal Wubshit <yeabkal@google.com>2024-01-29 15:28:51 -0800
committerYeabkal Wubshit <yeabkal@google.com>2024-02-05 21:02:45 +0000
commite6a5a4a92145207e3b3c1a6cc3f96a18953dab88 (patch)
treee6d70668bff39db23bc6ec18002fe158a16a065b
parentd98ccfc7b182660dec53c99602afee7907876dd9 (diff)
downloadConfigInfrastructure-e6a5a4a92145207e3b3c1a6cc3f96a18953dab88.tar.gz
Support dependency on charging for unattended reboot
The current mechanism of scheduled unattended reboots happening at a fixed time window are not working well for Wear, since users maybe wearing the watch and sleeping while this reboot triggers, causing disturbance due to audio/vibration from the reboot. This change implements a generic feature that can be used by any device that wishes to require charging when an unattended reboot happens. The config is set to off by default, so that other devices remain unaffected. We will enable the config for Wear. Bug: 322076175 Test: unit tests Change-Id: Icb7fdca1d1cdc25b0854d09b8de5e6dfc03b99bf
-rw-r--r--service/ServiceResources/res/values/config.xml3
-rw-r--r--service/flags.aconfig7
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManager.java43
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java2
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java146
5 files changed, 200 insertions, 1 deletions
diff --git a/service/ServiceResources/res/values/config.xml b/service/ServiceResources/res/values/config.xml
index a582b6f..b116767 100644
--- a/service/ServiceResources/res/values/config.xml
+++ b/service/ServiceResources/res/values/config.xml
@@ -37,4 +37,7 @@
<!-- 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 3b431d9..f8a8d25 100644
--- a/service/flags.aconfig
+++ b/service/flags.aconfig
@@ -26,3 +26,10 @@ flag {
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/UnattendedRebootManager.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
index 34f4602..4b7159a 100644
--- a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
@@ -1,5 +1,6 @@
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;
@@ -17,12 +18,15 @@ import android.net.ConnectivityManager;
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;
@@ -68,6 +72,17 @@ final class UnattendedRebootManager {
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*/
@@ -129,6 +144,11 @@ final class UnattendedRebootManager {
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);
@@ -292,6 +312,13 @@ final class UnattendedRebootManager {
scheduleReboot();
}
+ if (enableChargerDependencyForReboot()
+ && mInjector.requiresChargingForReboot(mContext)
+ && !isCharging(mContext)) {
+ triggerRebootOnCharging();
+ return;
+ }
+
// Proceed with RoR.
Log.v(TAG, "Rebooting device to apply device config flags.");
try {
@@ -326,6 +353,16 @@ final class UnattendedRebootManager {
}
}
+ 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);
@@ -337,6 +374,12 @@ final class UnattendedRebootManager {
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 @@ interface UnattendedRebootManagerInjector {
boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException;
+ boolean requiresChargingForReboot(Context context);
+
/** Regular reboot injector. */
void regularReboot(Context context);
}
diff --git a/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
index 72ad00d..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,7 @@
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;
@@ -12,6 +13,7 @@ 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;
@@ -24,18 +26,25 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
+import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
+import android.os.BatteryManager;
import android.util.Log;
import androidx.test.filters.SmallTest;
import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
@SmallTest
public class UnattendedRebootManagerTest {
@@ -54,7 +63,10 @@ public class UnattendedRebootManagerTest {
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;
@@ -68,11 +80,14 @@ public class UnattendedRebootManagerTest {
@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);
@@ -84,9 +99,18 @@ public class UnattendedRebootManagerTest {
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();
@@ -107,6 +131,9 @@ public class UnattendedRebootManagerTest {
Context.RECEIVER_EXPORTED);
mFakeInjector.setElapsedRealtime(ELAPSED_REALTIME_1_DAY);
+
+ mFakeInjector.setRequiresChargingForReboot(true);
+ when(mBatterManager.isCharging()).thenReturn(true);
}
@Test
@@ -130,6 +157,93 @@ public class UnattendedRebootManagerTest {
}
@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);
@@ -298,6 +412,7 @@ public class UnattendedRebootManagerTest {
static class FakeInjector implements UnattendedRebootManagerInjector {
private boolean isPreparedForUnattendedReboot;
+ private boolean requiresChargingForReboot;
private boolean rebootAndApplied;
private boolean regularRebooted;
private boolean requestedNetwork;
@@ -327,6 +442,15 @@ public class UnattendedRebootManagerTest {
}
@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;
@@ -438,4 +562,24 @@ public class UnattendedRebootManagerTest {
}
}
}
+
+ 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;
+ }
+ }
}