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