summaryrefslogtreecommitdiff
path: root/service/javatests
diff options
context:
space:
mode:
Diffstat (limited to 'service/javatests')
-rw-r--r--service/javatests/Android.bp59
-rw-r--r--service/javatests/AndroidManifest.xml34
-rw-r--r--service/javatests/AndroidTest.xml46
-rw-r--r--service/javatests/data/bootstrap1.txt3
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java96
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/DeviceConfigBootstrapValuesTest.java63
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/SimPinReplayManagerTest.java177
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java408
8 files changed, 886 insertions, 0 deletions
diff --git a/service/javatests/Android.bp b/service/javatests/Android.bp
new file mode 100644
index 0000000..0177890
--- /dev/null
+++ b/service/javatests/Android.bp
@@ -0,0 +1,59 @@
+// Copyright (C) 2023 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.
+
+filegroup {
+ name: "service-configinfrastructure-unitttests-bootstrap-files",
+ srcs: [
+ "data/*",
+ ],
+}
+
+android_test {
+ name: "ConfigInfrastructureServiceUnitTests",
+ min_sdk_version: "34",
+ sdk_version: "module_current",
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
+ data: [
+ ":service-configinfrastructure-unitttests-bootstrap-files",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ test_suites: [
+ "mts-configinfrastructure",
+ "general-tests",
+ ],
+ static_libs: [
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "androidx.annotation_annotation",
+ "modules-utils-build",
+ "service-configinfrastructure.impl",
+ "frameworks-base-testutils",
+ "mockito-target-minus-junit4",
+ "truth",
+ "flag-junit",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ "framework-connectivity.stubs.module_lib",
+ "framework-configinfrastructure",
+ ],
+ // Test coverage system runs on different devices. Need to
+ // compile for all architecture.
+ compile_multilib: "both",
+}
diff --git a/service/javatests/AndroidManifest.xml b/service/javatests/AndroidManifest.xml
new file mode 100644
index 0000000..663e401
--- /dev/null
+++ b/service/javatests/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.deviceconfig">
+
+ <uses-sdk android:minSdkVersion="34" android:targetSdkVersion="34" />
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+ <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.deviceconfig"
+ android:label="Tests for DeviceConfig service">
+ </instrumentation>
+
+</manifest>
diff --git a/service/javatests/AndroidTest.xml b/service/javatests/AndroidTest.xml
new file mode 100644
index 0000000..9c69017
--- /dev/null
+++ b/service/javatests/AndroidTest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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
+ -->
+<configuration description="Config for DeviceConfig service test cases">
+ <option name="test-suite-tag" value="mts" />
+ <option name="config-descriptor:metadata" key="component" value="service" />
+ <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <option name="config-descriptor:metadata" key="mainline-param"
+ value="com.google.android.configinfrastructure.apex" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="ConfigInfrastructureServiceUnitTests.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="settings put global device_config_sync_disabled 0" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="cleanup" value="true" />
+ <option name="push-file" key="bootstrap1.txt"
+ value="/data/local/tmp/deviceconfig/bootstrap1.txt" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.server.deviceconfig" />
+ <option name="runtime-hint" value="1m" />
+ </test>
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name"
+ value="com.google.android.configinfrastructure" />
+ </object>
+</configuration> \ No newline at end of file
diff --git a/service/javatests/data/bootstrap1.txt b/service/javatests/data/bootstrap1.txt
new file mode 100644
index 0000000..3349679
--- /dev/null
+++ b/service/javatests/data/bootstrap1.txt
@@ -0,0 +1,3 @@
+a.a.a:b.b.b=enabled
+a.a.a:b.b=disabled
+b.b.b:c.c=enabled \ No newline at end of file
diff --git a/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java b/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
new file mode 100644
index 0000000..40e807a
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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 android.content.Context;
+import android.app.AlarmManager;
+import android.content.ContextWrapper;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+
+@RunWith(AndroidJUnit4.class)
+public class BootNotificationCreatorTest {
+ Context mockContext;
+ AlarmManager mockAlarmManager;
+
+ BootNotificationCreator bootNotificationCreator;
+
+ @Before
+ public void setUp() {
+ mockAlarmManager = mock(AlarmManager.class);
+ mockContext = new ContextWrapper(getInstrumentation().getTargetContext()) {
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.ALARM_SERVICE)) {
+ return mockAlarmManager;
+ } else {
+ return super.getSystemService(name);
+ }
+ }
+ };
+
+ Map<String, Set<String>> testAconfigFlags = new HashMap<>();
+ testAconfigFlags.put("test", new HashSet<>());
+ testAconfigFlags.get("test").add("flag");
+
+ bootNotificationCreator = new BootNotificationCreator(mockContext, testAconfigFlags);
+ }
+
+ @Test
+ public void testNotificationScheduledWhenAconfigFlagStaged() {
+ HashMap<String, String> flags = new HashMap();
+ flags.put("test*flag", "value");
+ Properties properties = new Properties("staged", flags);
+
+ bootNotificationCreator.onPropertiesChanged(properties);
+
+ Mockito.verify(mockAlarmManager).setExact(anyInt(), anyLong(), any());
+ }
+
+ @Test
+ public void testNotificationNotScheduledForNonAconfigFlag() {
+ HashMap<String, String> flags = new HashMap();
+ flags.put("not_aconfig*flag", "value");
+ Properties properties = new Properties("staged", flags);
+
+ bootNotificationCreator.onPropertiesChanged(properties);
+
+ Mockito.verify(mockAlarmManager, times(0)).setExact(anyInt(), anyLong(), any());
+ }
+}
diff --git a/service/javatests/src/com/android/server/deviceconfig/DeviceConfigBootstrapValuesTest.java b/service/javatests/src/com/android/server/deviceconfig/DeviceConfigBootstrapValuesTest.java
new file mode 100644
index 0000000..9d77e8c
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/DeviceConfigBootstrapValuesTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.provider.DeviceConfig;
+
+import com.android.modules.utils.build.SdkLevel;
+
+import com.android.server.deviceconfig.DeviceConfigBootstrapValues;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import java.io.IOException;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class DeviceConfigBootstrapValuesTest {
+ private static final String WRITE_DEVICE_CONFIG_PERMISSION =
+ "android.permission.WRITE_DEVICE_CONFIG";
+
+ private static final String READ_DEVICE_CONFIG_PERMISSION =
+ "android.permission.READ_DEVICE_CONFIG";
+
+ private static final String PATH_1 = "file:///data/local/tmp/deviceconfig/bootstrap1.txt";
+
+ @Test
+ public void assertParsesFiles() throws IOException {
+ assumeTrue(SdkLevel.isAtLeastV());
+ InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+ WRITE_DEVICE_CONFIG_PERMISSION, READ_DEVICE_CONFIG_PERMISSION);
+
+ DeviceConfigBootstrapValues values = new DeviceConfigBootstrapValues(PATH_1);
+ values.applyValuesIfNeeded();
+
+ assertTrue(DeviceConfig.getBoolean("a.a.a", "b.b.b", false));
+ assertFalse(DeviceConfig.getBoolean("a.a.a", "b.b", true));
+ assertTrue(DeviceConfig.getBoolean("b.b.b", "c.c", false));
+ assertEquals(2, DeviceConfig.getProperties("a.a.a").getKeyset().size());
+ assertEquals(1, DeviceConfig.getProperties("b.b.b").getKeyset().size());
+ }
+}
diff --git a/service/javatests/src/com/android/server/deviceconfig/SimPinReplayManagerTest.java b/service/javatests/src/com/android/server/deviceconfig/SimPinReplayManagerTest.java
new file mode 100644
index 0000000..f56a6dd
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/SimPinReplayManagerTest.java
@@ -0,0 +1,177 @@
+package com.android.server.deviceconfig;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+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 android.content.Context;
+import android.content.ContextWrapper;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class SimPinReplayManagerTest {
+
+ private static final String TAG = "SimPinReplayManagerTest";
+
+ // A copy of the hidden field CarrierConfigManager#KEY_STORE_SIM_PIN_FOR_UNATTENDED_REBOOT_BOOL.
+ public static final String CARRIER_ENABLE_SIM_PIN_STORAGE_KEY =
+ "store_sim_pin_for_unattended_reboot_bool";
+
+ SimPinReplayManager mSimPinReplayManager;
+ SubscriptionManager mSubscriptionManager;
+ TelephonyManager mTelephonyManager;
+ CarrierConfigManager mCarrierConfigManager;
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+
+ mSubscriptionManager = mock(SubscriptionManager.class);
+ mTelephonyManager = mock(TelephonyManager.class);
+ mCarrierConfigManager = mock(CarrierConfigManager.class);
+
+ mContext =
+ new ContextWrapper(getInstrumentation().getTargetContext()) {
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.TELEPHONY_SUBSCRIPTION_SERVICE)) {
+ return mSubscriptionManager;
+ } else if (name.equals(Context.TELEPHONY_SERVICE)) {
+ return mTelephonyManager;
+ } else if (name.equals(Context.CARRIER_CONFIG_SERVICE)) {
+ return mCarrierConfigManager;
+ }
+ return super.getSystemService(name);
+ }
+ };
+
+ mSimPinReplayManager = new SimPinReplayManager(mContext);
+ }
+
+ @Test
+ public void prepareSimPinReplay_success() {
+ Log.i(TAG, "prepareSimPinReplay_success");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {1}); // has sim
+ TelephonyManager subIdManager = mock(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(1)).thenReturn(subIdManager);
+ when(subIdManager.isIccLockEnabled()).thenReturn(true); // has pin
+ PersistableBundle config = new PersistableBundle(); // empty carrier config
+ when(mCarrierConfigManager.getConfigForSubId(1, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY))
+ .thenReturn(config);
+ when(mTelephonyManager.prepareForUnattendedReboot())
+ .thenReturn(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS);
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertTrue(isPrepared);
+ }
+
+ @Test
+ public void prepareSimPinReplay_noSim() {
+ Log.i(TAG, "prepareSimPinReplay_noSim");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {}); // no sim
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertTrue(isPrepared);
+ }
+
+ @Test
+ public void prepareSimPinReplay_noSimPin() {
+ Log.i(TAG, "prepareSimPinReplay_noSimPin");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {1}); // has sim
+ TelephonyManager subIdManager = mock(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(1)).thenReturn(subIdManager);
+ when(subIdManager.isIccLockEnabled()).thenReturn(false); // no pin
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertTrue(isPrepared);
+ }
+
+ @Test
+ public void prepareSimPinReplay_carrierDisableSimPin() {
+ Log.i(TAG, "prepareSimPinReplay_carrierDisableSimPin");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {1}); // has sim
+ TelephonyManager subIdManager = mock(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(1)).thenReturn(subIdManager);
+ when(subIdManager.isIccLockEnabled()).thenReturn(true); // has pin
+ PersistableBundle config = new PersistableBundle();
+ config.putBoolean(CARRIER_ENABLE_SIM_PIN_STORAGE_KEY, false); // carrier disabled
+ when(mCarrierConfigManager.getConfigForSubId(1, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY))
+ .thenReturn(config);
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertFalse(isPrepared);
+ }
+
+ @Test
+ public void prepareSimPinReplay_carrierEnabled() {
+ Log.i(TAG, "prepareSimPinReplay_carrierEnabled");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {1}); // has sim
+ TelephonyManager subIdManager = mock(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(1)).thenReturn(subIdManager);
+ when(subIdManager.isIccLockEnabled()).thenReturn(true); // has pin
+ PersistableBundle config = new PersistableBundle();
+ config.putBoolean(CARRIER_ENABLE_SIM_PIN_STORAGE_KEY, true); // carrier enabled
+ when(mCarrierConfigManager.getConfigForSubId(1, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY))
+ .thenReturn(config);
+ when(mTelephonyManager.prepareForUnattendedReboot())
+ .thenReturn(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS);
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertTrue(isPrepared);
+ }
+
+ @Test
+ public void prepareSimPinReplay_prepareError() {
+ Log.i(TAG, "prepareSimPinReplay_prepareError");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {1}); // has sim
+ TelephonyManager subIdManager = mock(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(1)).thenReturn(subIdManager);
+ when(subIdManager.isIccLockEnabled()).thenReturn(true); // has pin
+ PersistableBundle config = new PersistableBundle();
+ config.putBoolean(CARRIER_ENABLE_SIM_PIN_STORAGE_KEY, true); // carrier enabled
+ when(mCarrierConfigManager.getConfigForSubId(1, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY))
+ .thenReturn(config);
+ when(mTelephonyManager.prepareForUnattendedReboot())
+ .thenReturn(TelephonyManager.PREPARE_UNATTENDED_REBOOT_ERROR);
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertFalse(isPrepared);
+ }
+
+ @Test
+ public void prepareSimPinReplay_preparePinRequired() {
+ Log.i(TAG, "prepareSimPinReplay_preparePinRequired");
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[] {1}); // has sim
+ TelephonyManager subIdManager = mock(TelephonyManager.class);
+ when(mTelephonyManager.createForSubscriptionId(1)).thenReturn(subIdManager);
+ when(subIdManager.isIccLockEnabled()).thenReturn(true); // has pin
+ PersistableBundle config = new PersistableBundle();
+ config.putBoolean(CARRIER_ENABLE_SIM_PIN_STORAGE_KEY, true); // carrier enabled
+ when(mCarrierConfigManager.getConfigForSubId(1, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY))
+ .thenReturn(config);
+ when(mTelephonyManager.prepareForUnattendedReboot())
+ .thenReturn(TelephonyManager.PREPARE_UNATTENDED_REBOOT_PIN_REQUIRED);
+
+ boolean isPrepared = mSimPinReplayManager.prepareSimPinReplay();
+
+ assertFalse(isPrepared);
+ }
+}
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..0ddbc64
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java
@@ -0,0 +1,408 @@
+package com.android.server.deviceconfig;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+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;
+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.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.flag.junit.SetFlagsRule;
+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.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+import java.time.ZoneId;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+@SmallTest
+public class UnattendedRebootManagerTest {
+
+ private static final String TAG = "UnattendedRebootManagerTest";
+
+ private static final int REBOOT_FREQUENCY = 1;
+ private static final int REBOOT_START_HOUR = 2;
+ private static final int REBOOT_END_HOUR = 3;
+
+ private static final long CURRENT_TIME = 1696452549304L; // 2023-10-04T13:49:09.304
+ private static final long REBOOT_TIME = 1696497120000L; // 2023-10-05T02:12:00
+ private static final long RESCHEDULED_REBOOT_TIME = 1696583520000L; // 2023-10-06T02:12:00
+ private static final long OUTSIDE_WINDOW_REBOOT_TIME = 1696587000000L; // 2023-10-06T03:10:00
+ private static final long RESCHEDULED_OUTSIDE_WINDOW_REBOOT_TIME =
+ 1696669920000L; // 2023-10-07T02:12:00
+ private static final long ELAPSED_REALTIME_1_DAY = 86400000L;
+
+ private Context mContext;
+ private KeyguardManager mKeyguardManager;
+ private ConnectivityManager mConnectivityManager;
+ private FakeInjector mFakeInjector;
+ private UnattendedRebootManager mRebootManager;
+ private SimPinReplayManager mSimPinReplayManager;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+
+ @Before
+ public void setUp() throws Exception {
+ mSetFlagsRule.enableFlags(FLAG_ENABLE_SIM_PIN_REPLAY);
+
+ mSimPinReplayManager = mock(SimPinReplayManager.class);
+ mKeyguardManager = mock(KeyguardManager.class);
+ mConnectivityManager = mock(ConnectivityManager.class);
+
+ mContext =
+ new ContextWrapper(getInstrumentation().getTargetContext()) {
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.KEYGUARD_SERVICE)) {
+ return mKeyguardManager;
+ } else if (name.equals(Context.CONNECTIVITY_SERVICE)) {
+ return mConnectivityManager;
+ }
+ return super.getSystemService(name);
+ }
+ };
+
+ mFakeInjector = new FakeInjector();
+ mRebootManager = new UnattendedRebootManager(mContext, mFakeInjector, mSimPinReplayManager);
+
+ // 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);
+
+ mFakeInjector.setElapsedRealtime(ELAPSED_REALTIME_1_DAY);
+ }
+
+ @Test
+ public void scheduleReboot() {
+ Log.i(TAG, "scheduleReboot");
+ 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);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertTrue(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ }
+
+ @Test
+ public void scheduleReboot_noPinLock() {
+ Log.i(TAG, "scheduleReboot_noPinLock");
+ when(mKeyguardManager.isDeviceSecure()).thenReturn(false);
+ when(mConnectivityManager.getNetworkCapabilities(any()))
+ .thenReturn(
+ new NetworkCapabilities.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+ .build());
+ when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertTrue(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ }
+
+ @Test
+ public void scheduleReboot_noPreparation() {
+ Log.i(TAG, "scheduleReboot_noPreparation");
+ 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);
+
+ mRebootManager.scheduleReboot();
+
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(RESCHEDULED_REBOOT_TIME);
+ }
+
+ @Test
+ public void scheduleReboot_simPinPreparationFailed() {
+ Log.i(TAG, "scheduleReboot");
+ 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(false).thenReturn(true);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertTrue(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(RESCHEDULED_REBOOT_TIME);
+ }
+
+ @Test
+ public void scheduleReboot_noInternet() {
+ Log.i(TAG, "scheduleReboot_noInternet");
+ when(mKeyguardManager.isDeviceSecure()).thenReturn(true);
+ when(mConnectivityManager.getNetworkCapabilities(any())).thenReturn(new NetworkCapabilities());
+ when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ assertTrue(mFakeInjector.isRequestedNetwork());
+ }
+
+ @Test
+ public void scheduleReboot_noInternetValidation() {
+ Log.i(TAG, "scheduleReboot_noInternetValidation");
+ when(mKeyguardManager.isDeviceSecure()).thenReturn(true);
+ when(mConnectivityManager.getNetworkCapabilities(any()))
+ .thenReturn(
+ new NetworkCapabilities.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build());
+ when(mSimPinReplayManager.prepareSimPinReplay()).thenReturn(true);
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(REBOOT_TIME);
+ assertTrue(mFakeInjector.isRequestedNetwork());
+ }
+
+ @Test
+ public void scheduleReboot_elapsedRealtimeLessThanFrequency() {
+ Log.i(TAG, "scheduleReboot_elapsedRealtimeLessThanFrequency");
+ 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);
+ mFakeInjector.setElapsedRealtime(82800000); // 23 hours
+
+ mRebootManager.prepareUnattendedReboot();
+ mRebootManager.scheduleReboot();
+
+ assertFalse(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime()).isEqualTo(RESCHEDULED_REBOOT_TIME);
+ }
+
+ @Test
+ public void tryRebootOrSchedule_outsideRebootWindow() {
+ Log.i(TAG, "scheduleReboot_internetOutsideRebootWindow");
+ 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);
+ mFakeInjector.setNow(OUTSIDE_WINDOW_REBOOT_TIME);
+
+ mRebootManager.prepareUnattendedReboot();
+ // Simulating case when reboot is tried after network connection is established outside the
+ // reboot window.
+ mRebootManager.tryRebootOrSchedule();
+
+ assertTrue(mFakeInjector.isRebootAndApplied());
+ assertFalse(mFakeInjector.isRegularRebooted());
+ assertThat(mFakeInjector.getActualRebootTime())
+ .isEqualTo(RESCHEDULED_OUTSIDE_WINDOW_REBOOT_TIME);
+ }
+
+ static class FakeInjector implements UnattendedRebootManagerInjector {
+
+ private boolean isPreparedForUnattendedReboot;
+ private boolean rebootAndApplied;
+ private boolean regularRebooted;
+ private boolean requestedNetwork;
+ private long actualRebootTime;
+ private boolean scheduledReboot;
+
+ private long nowMillis;
+
+ private long elapsedRealtimeMillis;
+
+ FakeInjector() {
+ nowMillis = CURRENT_TIME;
+ }
+
+ @Override
+ public void prepareForUnattendedUpdate(
+ @NonNull Context context,
+ @NonNull String updateToken,
+ @Nullable IntentSender intentSender) {
+ context.sendBroadcast(new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED));
+ isPreparedForUnattendedReboot = true;
+ }
+
+ @Override
+ public boolean isPreparedForUnattendedUpdate(@NonNull Context context) {
+ return isPreparedForUnattendedReboot;
+ }
+
+ @Override
+ public int rebootAndApply(
+ @NonNull Context context, @NonNull String reason, boolean slotSwitch) {
+ rebootAndApplied = true;
+ return 0; // No error.
+ }
+
+ @Override
+ public int getRebootFrequency() {
+ return REBOOT_FREQUENCY;
+ }
+
+ @Override
+ public void setRebootAlarm(Context context, long rebootTimeMillis) {
+ // To prevent infinite loop, do not simulate another reboot if reboot was already scheduled.
+ if (scheduledReboot) {
+ actualRebootTime = rebootTimeMillis;
+ return;
+ }
+ // Advance now to reboot time and reboot immediately.
+ scheduledReboot = true;
+ actualRebootTime = rebootTimeMillis;
+ setNow(rebootTimeMillis);
+
+ LatchingBroadcastReceiver rebootReceiver = new LatchingBroadcastReceiver();
+
+ // Wait for reboot broadcast to be sent.
+ context.sendOrderedBroadcast(
+ new Intent(ACTION_TRIGGER_REBOOT), null, rebootReceiver, null, 0, null, null);
+
+ rebootReceiver.await(20, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void triggerRebootOnNetworkAvailable(Context context) {
+ requestedNetwork = true;
+ }
+
+ public boolean isRequestedNetwork() {
+ return requestedNetwork;
+ }
+
+ @Override
+ public int getRebootStartTime() {
+ return REBOOT_START_HOUR;
+ }
+
+ @Override
+ public int getRebootEndTime() {
+ return REBOOT_END_HOUR;
+ }
+
+ @Override
+ public long now() {
+ return nowMillis;
+ }
+
+ public void setNow(long nowMillis) {
+ this.nowMillis = nowMillis;
+ }
+
+ @Override
+ public ZoneId zoneId() {
+ return ZoneId.of("America/Los_Angeles");
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return elapsedRealtimeMillis;
+ }
+
+ public void setElapsedRealtime(long elapsedRealtimeMillis) {
+ this.elapsedRealtimeMillis = elapsedRealtimeMillis;
+ }
+
+ @Override
+ public void regularReboot(Context context) {
+ 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) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}