summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVeena Arvind <aveena@google.com>2023-10-17 20:44:48 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2023-10-17 20:44:48 +0000
commitf4466f8cb1b0ce9ff01f522c4bd592e91cfb549f (patch)
tree8af545e2dbd8b8408f935c7293cb7ec2edaa5307
parent206474daf82d326e2eb5f7281f30d090b732ef40 (diff)
parentf55b40f6ca866354123380198c8ad040749f7638 (diff)
downloadConfigInfrastructure-f4466f8cb1b0ce9ff01f522c4bd592e91cfb549f.tar.gz
Merge "Add unattended reboot functionality to apply flags" into main
-rw-r--r--service/flags.aconfig6
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigInit.java116
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManager.java223
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java43
-rw-r--r--service/javatests/Android.bp5
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java214
6 files changed, 552 insertions, 55 deletions
diff --git a/service/flags.aconfig b/service/flags.aconfig
index 6240af1..a9ca293 100644
--- a/service/flags.aconfig
+++ b/service/flags.aconfig
@@ -6,3 +6,9 @@ flag {
description: "If enabled, a notification appears when flags are staged to be applied on reboot."
bug: "296462695"
}
+flag {
+ name: "enable_unattended_reboot"
+ namespace: "core_experiments_team_internal"
+ description: "This flag controls enabling the unattended reboot feature for applying flags."
+ bug: "297502146"
+}
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
index 42cee70..6336619 100644
--- a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
+++ b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
@@ -1,84 +1,92 @@
package com.android.server.deviceconfig;
-import java.io.FileDescriptor;
-import java.io.IOException;
+import static com.android.server.deviceconfig.Flags.enableRebootNotification;
+import static com.android.server.deviceconfig.Flags.enableUnattendedReboot;
+import java.io.IOException;
import android.content.Intent;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Binder;
+import android.content.IntentFilter;
import android.provider.DeviceConfig;
-import android.provider.DeviceConfigManager;
import android.provider.UpdatableDeviceConfigServiceReadiness;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.content.ComponentName;
-
-import android.provider.aidl.IDeviceConfigManager;
import android.util.Slog;
-
import com.android.modules.utils.build.SdkLevel;
-
import com.android.server.SystemService;
-import static com.android.server.deviceconfig.Flags.enableRebootNotification;
-
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public class DeviceConfigInit {
- private static final String TAG = "DEVICE_CONFIG_INIT";
- private static final String STAGED_NAMESPACE = "staged";
+ private static final String TAG = "DEVICE_CONFIG_INIT";
+ private static final String STAGED_NAMESPACE = "staged";
- private DeviceConfigInit() {
- // do not instantiate
- }
+ private DeviceConfigInit() {
+ // do not instantiate
+ }
+
+ /** @hide */
+ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+ public static class Lifecycle extends SystemService {
+ private DeviceConfigServiceImpl mService;
+ private UnattendedRebootManager mUnattendedRebootManager;
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
- public static class Lifecycle extends SystemService {
- private DeviceConfigServiceImpl mService;
-
- /** @hide */
- @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
- public Lifecycle(@NonNull Context context) {
- super(context);
- // this service is always instantiated but should only launch subsequent service(s)
- // if the module is ready
- if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) {
- mService = new DeviceConfigServiceImpl(getContext());
- publishBinderService(DeviceConfig.SERVICE_NAME, mService);
- }
- applyBootstrapValues();
- }
+ public Lifecycle(@NonNull Context context) {
+ super(context);
+ // this service is always instantiated but should only launch subsequent service(s)
+ // if the module is ready
+ if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) {
+ mService = new DeviceConfigServiceImpl(getContext());
+ publishBinderService(DeviceConfig.SERVICE_NAME, mService);
+ }
+ applyBootstrapValues();
+ }
- /**
- * @hide
- */
- @Override
- public void onStart() {
- if (enableRebootNotification()) {
- DeviceConfig.addOnPropertiesChangedListener(
- STAGED_NAMESPACE,
- AsyncTask.THREAD_POOL_EXECUTOR,
- new BootNotificationCreator(getContext().getApplicationContext()));
- }
- }
+ /** @hide */
+ @Override
+ public void onStart() {
+ if (enableRebootNotification()) {
+ DeviceConfig.addOnPropertiesChangedListener(
+ STAGED_NAMESPACE,
+ AsyncTask.THREAD_POOL_EXECUTOR,
+ new BootNotificationCreator(getContext().getApplicationContext()));
+ }
+ if (enableUnattendedReboot()) {
+ mUnattendedRebootManager =
+ new UnattendedRebootManager(getContext().getApplicationContext());
+ getContext()
+ .registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mUnattendedRebootManager.prepareUnattendedReboot();
+ mUnattendedRebootManager.scheduleReboot();
+ }
+ },
+ new IntentFilter(Intent.ACTION_BOOT_COMPLETED));
+ }
+ }
- private void applyBootstrapValues() {
- if (SdkLevel.isAtLeastV()) {
- try {
- new DeviceConfigBootstrapValues().applyValuesIfNeeded();
- } catch (RuntimeException e) {
- Slog.e(TAG, "Failed to load boot overrides", e);
- throw e;
- } catch (IOException e) {
- Slog.e(TAG, "Failed to load boot overrides", e);
- throw new RuntimeException(e);
- }
- }
+ private void applyBootstrapValues() {
+ if (SdkLevel.isAtLeastV()) {
+ try {
+ new DeviceConfigBootstrapValues().applyValuesIfNeeded();
+ } catch (RuntimeException e) {
+ Slog.e(TAG, "Failed to load boot overrides", e);
+ throw e;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to load boot overrides", e);
+ throw new RuntimeException(e);
}
+ }
}
-
+ }
}
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
new file mode 100644
index 0000000..c360f4d
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
@@ -0,0 +1,223 @@
+package com.android.server.deviceconfig;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+/**
+ * Reboot scheduler for applying aconfig flags.
+ *
+ * <p>If device is password protected, uses <a
+ * href="https://source.android.com/docs/core/ota/resume-on-reboot">Resume on Reboot</a> to reboot
+ * the device, otherwise proceeds with regular reboot.
+ *
+ * @hide
+ */
+final class UnattendedRebootManager {
+ private static final int DEFAULT_REBOOT_WINDOW_START_TIME_HOUR = 2;
+
+ private static final int DEFAULT_REBOOT_FREQUENCY_DAYS = 2;
+
+ private static final String TAG = "UnattendedRebootManager";
+
+ static final String REBOOT_REASON = "deviceconfig";
+
+ @VisibleForTesting
+ static final String ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED =
+ "com.android.server.deviceconfig.RESUME_ON_REBOOOT_LSKF_CAPTURED";
+
+ @VisibleForTesting
+ static final String ACTION_TRIGGER_REBOOT = "com.android.server.deviceconfig.TRIGGER_REBOOT";
+
+ private final Context mContext;
+
+ private boolean mLskfCaptured;
+
+ private final UnattendedRebootManagerInjector mInjector;
+
+ private static class InjectorImpl implements UnattendedRebootManagerInjector {
+ InjectorImpl() {
+ /*no op*/
+ }
+
+ public long now() {
+ return System.currentTimeMillis();
+ }
+
+ public ZoneId zoneId() {
+ return ZoneId.systemDefault();
+ }
+
+ public int getRebootStartTime() {
+ return DEFAULT_REBOOT_WINDOW_START_TIME_HOUR;
+ }
+
+ public int getRebootFrequency() {
+ return DEFAULT_REBOOT_FREQUENCY_DAYS;
+ }
+
+ public void setRebootAlarm(Context context, long rebootTimeMillis) {
+ AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 0,
+ new Intent(ACTION_TRIGGER_REBOOT),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, rebootTimeMillis, pendingIntent);
+ }
+
+ public int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch)
+ throws IOException {
+ return RecoverySystem.rebootAndApply(context, reason, slotSwitch);
+ }
+
+ public void prepareForUnattendedUpdate(
+ @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender)
+ throws IOException {
+ RecoverySystem.prepareForUnattendedUpdate(context, updateToken, intentSender);
+ }
+
+ public boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException {
+ return RecoverySystem.isPreparedForUnattendedUpdate(context);
+ }
+
+ public void regularReboot(Context context) {
+ PowerManager powerManager = context.getSystemService(PowerManager.class);
+ powerManager.reboot(REBOOT_REASON);
+ }
+ }
+
+ @VisibleForTesting
+ UnattendedRebootManager(Context context, UnattendedRebootManagerInjector injector) {
+ mContext = context;
+ mInjector = injector;
+
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mLskfCaptured = true;
+ }
+ },
+ new IntentFilter(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED),
+ Context.RECEIVER_EXPORTED);
+
+ // Do not export receiver so that tests don't trigger reboot.
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ tryRebootOrSchedule();
+ }
+ },
+ new IntentFilter(ACTION_TRIGGER_REBOOT),
+ Context.RECEIVER_NOT_EXPORTED);
+ }
+
+ UnattendedRebootManager(Context context) {
+ this(context, new InjectorImpl());
+ }
+
+ public void prepareUnattendedReboot() {
+ Log.i(TAG, "Preparing for Unattended Reboot");
+ // RoR only supported on devices with screen lock.
+ if (!isDeviceSecure(mContext)) {
+ return;
+ }
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ mContext,
+ /* requestCode= */ 0,
+ new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+
+ try {
+ mInjector.prepareForUnattendedUpdate(
+ mContext, /* updateToken= */ "", pendingIntent.getIntentSender());
+ } catch (IOException e) {
+ Log.i(TAG, "prepareForUnattendedReboot failed with exception" + e.getLocalizedMessage());
+ }
+ }
+
+ public void scheduleReboot() {
+ // Reboot the next day at the reboot start time.
+ LocalDateTime timeToReboot =
+ Instant.ofEpochMilli(mInjector.now())
+ .atZone(mInjector.zoneId())
+ .toLocalDate()
+ .plusDays(mInjector.getRebootFrequency())
+ .atTime(mInjector.getRebootStartTime(), /* minute= */ 0);
+ long rebootTimeMillis = timeToReboot.atZone(mInjector.zoneId()).toInstant().toEpochMilli();
+ Log.v(TAG, "Scheduling unattended reboot at time " + timeToReboot);
+
+ if (timeToReboot.isBefore(
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(mInjector.now()), mInjector.zoneId()))) {
+ Log.w(TAG, "Reboot time has already passed.");
+ return;
+ }
+
+ mInjector.setRebootAlarm(mContext, rebootTimeMillis);
+ }
+
+ @VisibleForTesting
+ void tryRebootOrSchedule() {
+ // TODO(b/305259443): check network is connected
+ // Check if RoR is supported.
+ if (!isDeviceSecure(mContext)) {
+ Log.v(TAG, "Device is not secure. Proceed with regular reboot");
+ mInjector.regularReboot(mContext);
+ } else if (isPreparedForUnattendedReboot()) {
+ try {
+ mInjector.rebootAndApply(mContext, REBOOT_REASON, /* slotSwitch= */ false);
+ } catch (IOException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ }
+ // If reboot is successful, should not reach this.
+ } else {
+ // Lskf is not captured, try again the following day
+ prepareUnattendedReboot();
+ scheduleReboot();
+ }
+ }
+
+ private boolean isPreparedForUnattendedReboot() {
+ try {
+ boolean isPrepared = mInjector.isPreparedForUnattendedUpdate(mContext);
+ if (isPrepared != mLskfCaptured) {
+ Log.w(TAG, "isPrepared != mLskfCaptured. Received " + isPrepared);
+ }
+ return isPrepared;
+ } catch (IOException e) {
+ Log.w(TAG, e.getLocalizedMessage());
+ return mLskfCaptured;
+ }
+ }
+
+ /** Returns true if the device has screen lock. */
+ private static boolean isDeviceSecure(Context context) {
+ KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+ if (keyguardManager == null) {
+ // Unknown if device is locked, proceed with RoR anyway.
+ Log.w(TAG, "Keyguard manager is null, proceeding with RoR anyway.");
+ return true;
+ }
+ return keyguardManager.isDeviceSecure();
+ }
+}
diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
new file mode 100644
index 0000000..4e883d3
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
@@ -0,0 +1,43 @@
+package com.android.server.deviceconfig;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.IntentSender;
+import android.os.RecoverySystem;
+
+import java.io.IOException;
+import java.time.ZoneId;
+
+/**
+ * Dependency injectors for {@link com.android.server.deviceconfig.UnattendedRebootManager} to
+ * enable unit testing.
+ */
+interface UnattendedRebootManagerInjector {
+
+ /** Time injectors. */
+ long now();
+
+ ZoneId zoneId();
+
+ /** Reboot time injectors. */
+ int getRebootStartTime();
+
+ int getRebootFrequency();
+
+ /** Reboot Alarm injector. */
+ void setRebootAlarm(Context context, long rebootTimeMillis);
+
+ /** {@link RecoverySystem} methods injectors. */
+ int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch)
+ throws IOException;
+
+ void prepareForUnattendedUpdate(
+ @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender)
+ throws IOException;
+
+ boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException;
+
+ /** Regular reboot injector. */
+ void regularReboot(Context context);
+}
diff --git a/service/javatests/Android.bp b/service/javatests/Android.bp
index 047d146..eb1561c 100644
--- a/service/javatests/Android.bp
+++ b/service/javatests/Android.bp
@@ -36,11 +36,14 @@ android_test {
"general-tests",
],
static_libs: [
+ "androidx.test.rules",
"androidx.test.runner",
+ "androidx.annotation_annotation",
"modules-utils-build",
"service-configinfrastructure.impl",
- "truth",
+ "frameworks-base-testutils",
"mockito-target-minus-junit4",
+ "truth",
],
libs: [
"android.test.base",
diff --git a/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
new file mode 100644
index 0000000..f87cf56
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
@@ -0,0 +1,214 @@
+package com.android.server.deviceconfig;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static com.android.server.deviceconfig.UnattendedRebootManager.ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED;
+import static com.android.server.deviceconfig.UnattendedRebootManager.ACTION_TRIGGER_REBOOT;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.app.KeyguardManager;
+import android.content.BroadcastReceiver;
+import android.content.ContextWrapper;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import java.io.IOException;
+import java.time.ZoneId;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class UnattendedRebootManagerTest {
+
+ private static final int REBOOT_FREQUENCY = 1;
+ private static final int REBOOT_HOUR = 2;
+ private static final long CURRENT_TIME = 1696452549304L; // 2023-10-04T13:49:09.304
+ private static final long REBOOT_TIME = 1696496400000L; // 2023-10-05T02:00:00
+
+ private Context mContext;
+
+ private KeyguardManager mKeyguardManager;
+
+ FakeInjector mFakeInjector;
+
+ private UnattendedRebootManager mRebootManager;
+
+ @Before
+ public void setUp() throws Exception {
+ mKeyguardManager = mock(KeyguardManager.class);
+
+ mContext =
+ new ContextWrapper(getInstrumentation().getTargetContext()) {
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.KEYGUARD_SERVICE)) {
+ return mKeyguardManager;
+ }
+ return super.getSystemService(name);
+ }
+ };
+
+ mFakeInjector = new FakeInjector();
+ mRebootManager = new UnattendedRebootManager(mContext, mFakeInjector);
+
+ // Need to register receiver in tests so that the test doesn't trigger reboot requested by
+ // deviceconfig.
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mRebootManager.tryRebootOrSchedule();
+ }
+ },
+ new IntentFilter(ACTION_TRIGGER_REBOOT),
+ Context.RECEIVER_EXPORTED);
+ }
+
+ @Test
+ public void scheduleReboot() {
+ when(mKeyguardManager.isDeviceSecure()).thenReturn(true);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ assertTrue(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ }
+
+ @Test
+ public void scheduleReboot_noPinLock() {
+ when(mKeyguardManager.isDeviceSecure()).thenReturn(false);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertTrue(mFakeInjector.isRegularRebooted());
+ }
+
+ @Test
+ public void scheduleReboot_noPreparation() {
+ when(mKeyguardManager.isDeviceSecure()).thenReturn(true);
+
+ mRebootManager.scheduleReboot();
+
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ }
+
+ static class FakeInjector implements UnattendedRebootManagerInjector {
+
+ private boolean isPreparedForUnattendedReboot;
+ private boolean rebootAndApplied;
+ private boolean regularRebooted;
+ private long actualRebootTime;
+
+ FakeInjector() {}
+
+ @Override
+ public void prepareForUnattendedUpdate(
+ @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender)
+ throws IOException {
+ context.sendBroadcast(new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED));
+ isPreparedForUnattendedReboot = true;
+ }
+
+ @Override
+ public boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException {
+ return isPreparedForUnattendedReboot;
+ }
+
+ @Override
+ public int rebootAndApply(
+ @NonNull Context context, @NonNull String reason, boolean slotSwitch) {
+ Log.i("UnattendedRebootManagerTest", "MockInjector.rebootAndApply");
+ rebootAndApplied = true;
+ return 0; // No error.
+ }
+
+ @Override
+ public int getRebootFrequency() {
+ return REBOOT_FREQUENCY;
+ }
+
+ @Override
+ public void setRebootAlarm(Context context, long rebootTimeMillis) {
+ // reboot immediately
+ actualRebootTime = rebootTimeMillis;
+ context.sendBroadcast(new Intent(UnattendedRebootManager.ACTION_TRIGGER_REBOOT));
+
+ LatchingBroadcastReceiver rebootReceiver = new LatchingBroadcastReceiver();
+ context.registerReceiver(
+ rebootReceiver, new IntentFilter(ACTION_TRIGGER_REBOOT), Context.RECEIVER_EXPORTED);
+ rebootReceiver.await(10, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public int getRebootStartTime() {
+ return REBOOT_HOUR;
+ }
+
+ @Override
+ public long now() {
+ return CURRENT_TIME;
+ }
+
+ @Override
+ public ZoneId zoneId() {
+ return ZoneId.of("America/Los_Angeles");
+ }
+
+ @Override
+ public void regularReboot(Context context) {
+ Log.i("UnattendedRebootManagerTest", "MockInjector.regularRebooted");
+ regularRebooted = true;
+ }
+
+ boolean isRebootAndApplied() {
+ return rebootAndApplied;
+ }
+
+ boolean isRegularRebooted() {
+ return regularRebooted;
+ }
+
+ public long getActualRebootTime() {
+ return actualRebootTime;
+ }
+ }
+
+ /**
+ * A {@link BroadcastReceiver} with an internal latch that unblocks once any intent is received.
+ */
+ private static class LatchingBroadcastReceiver extends BroadcastReceiver {
+ private CountDownLatch latch = new CountDownLatch(1);
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ latch.countDown();
+ }
+
+ public boolean await(long timeoutInMs, TimeUnit timeUnit) {
+ try {
+ return latch.await(timeoutInMs, timeUnit);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+ }
+}