summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-10-19 01:27:32 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-10-19 01:27:32 +0000
commit21b1512869af7ae91ec86c550df7ecf4a39a92af (patch)
treee2b02def2954c867ab4792974acac49c17c4ddf2
parentdff4aabb1286e60e2acc4f7a340ee4a142b3f7c3 (diff)
parentb6487df1597b46b204d25b3498825dde13c09427 (diff)
downloadConfigInfrastructure-21b1512869af7ae91ec86c550df7ecf4a39a92af.tar.gz
Snap for 10970117 from b6487df1597b46b204d25b3498825dde13c09427 to 24Q1-release
Change-Id: I70b6adc8ec72c844071e337148866bdea5747b9b
-rw-r--r--service/Android.bp2
-rw-r--r--service/ServiceResources/AndroidManifest.xml4
-rw-r--r--service/ServiceResources/res/drawable/ic_restart.xml26
-rw-r--r--service/ServiceResources/res/values/strings.xml4
-rw-r--r--service/flags.aconfig6
-rw-r--r--service/jarjar-rules.txt2
-rw-r--r--service/java/com/android/server/deviceconfig/BootNotificationCreator.java45
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigInit.java70
-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/BootNotificationCreatorTest.java56
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/UnattendedRebootManagerTest.java214
13 files changed, 662 insertions, 38 deletions
diff --git a/service/Android.bp b/service/Android.bp
index 8d0fb79..125d655 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -26,6 +26,7 @@ java_sdk_library {
permitted_packages: [
"android.provider",
"com.android.server.deviceconfig",
+ //"com.google.protobuf",
],
apex_available: [
"com.android.configinfrastructure",
@@ -34,6 +35,7 @@ java_sdk_library {
"modules-utils-build",
"modules-utils-shell-command-handler",
"device_config_reboot_flags_java_lib",
+ "libaconfig_java_proto_lite"
],
libs: [
"framework-configinfrastructure.impl",
diff --git a/service/ServiceResources/AndroidManifest.xml b/service/ServiceResources/AndroidManifest.xml
index 4fc5e06..d4e2796 100644
--- a/service/ServiceResources/AndroidManifest.xml
+++ b/service/ServiceResources/AndroidManifest.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -21,7 +21,7 @@
package="com.android.server.deviceconfig.resources"
coreApp="true"
android:versionCode="1"
- android:versionName="R-initial">
+ android:versionName="V">
<application>
</application>
</manifest>
diff --git a/service/ServiceResources/res/drawable/ic_restart.xml b/service/ServiceResources/res/drawable/ic_restart.xml
new file mode 100644
index 0000000..24d7c34
--- /dev/null
+++ b/service/ServiceResources/res/drawable/ic_restart.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@android:color/white">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02c-2.83,-0.48 -5,-2.94 -5,-5.91zM20,13c0,-4.42 -3.58,-8 -8,-8 -0.06,0 -0.12,0.01 -0.18,0.01l1.09,-1.09L11.5,2.5 8,6l3.5,3.5 1.41,-1.41 -1.08,-1.08c0.06,0 0.12,-0.01 0.17,-0.01 3.31,0 6,2.69 6,6 0,2.97 -2.17,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93z"/>
+</vector>
diff --git a/service/ServiceResources/res/values/strings.xml b/service/ServiceResources/res/values/strings.xml
index 53407e0..ed60624 100644
--- a/service/ServiceResources/res/values/strings.xml
+++ b/service/ServiceResources/res/values/strings.xml
@@ -3,5 +3,7 @@
<!-- Title text for a notification that instructs the user to reboot to apply new server flags to the device [CHAR LIMIT=NONE] -->
<string name="boot_notification_title">New flags available</string>
<!-- Content text for a notification that instructs the user to reboot to apply new server flags to the device [CHAR LIMIT=NONE] -->
- <string name="boot_notification_content">Tap to reboot and apply new trunkfood flag values</string>
+ <string name="boot_notification_content">Your device has new trunkfood flags available. A restart is required to apply them.</string>
+ <!-- Action text for a button that instructs the user to reboot to apply new server flags to the device [CHAR LIMIT=NONE] -->
+ <string name="boot_notification_action_text">Tap to reboot</string>
</resources>
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/jarjar-rules.txt b/service/jarjar-rules.txt
index cf60d8f..ebaebab 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -1 +1,3 @@
rule com.android.modules.utils.** com.android.server.deviceconfig.internal.modules.utils.@1
+rule com.google.protobuf.** com.android.server.deviceconfig.internal.protobuf.@1
+rule android.aconfig.** com.android.server.deviceconfig.internal.aconfig.@1
diff --git a/service/java/com/android/server/deviceconfig/BootNotificationCreator.java b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java
index 388fedc..9772141 100644
--- a/service/java/com/android/server/deviceconfig/BootNotificationCreator.java
+++ b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java
@@ -3,6 +3,7 @@ package com.android.server.deviceconfig;
import android.annotation.NonNull;
import android.app.AlarmManager;
import android.app.Notification;
+import android.app.Notification.Action;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -21,11 +22,14 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
/**
- * Creates notifications when flags are staged on the device.
+ * Creates notifications when aconfig flags are staged on the device.
*
* The notification alerts the user to reboot, to apply the staged flags.
*
@@ -54,11 +58,15 @@ class BootNotificationCreator implements OnPropertiesChangedListener {
private Context context;
- private static final int REBOOT_HOUR = 18;
- private static final int REBOOT_MINUTE = 2;
+ private static final int REBOOT_HOUR = 10;
+ private static final int REBOOT_MINUTE = 0;
- public BootNotificationCreator(@NonNull Context context) {
+ private Map<String, Set<String>> aconfigFlags;
+
+ public BootNotificationCreator(@NonNull Context context,
+ Map<String, Set<String>> aconfigFlags) {
this.context = context;
+ this.aconfigFlags = aconfigFlags;
this.context.registerReceiver(
new HardRebootBroadcastReceiver(),
@@ -72,6 +80,10 @@ class BootNotificationCreator implements OnPropertiesChangedListener {
@Override
public void onPropertiesChanged(Properties properties) {
+ if (!containsAconfigChanges(properties)) {
+ return;
+ }
+
if (!tryInitializeDependenciesIfNeeded()) {
Slog.i(TAG, "not posting notif; service dependencies not ready");
return;
@@ -102,6 +114,25 @@ class BootNotificationCreator implements OnPropertiesChangedListener {
AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent);
}
+ private boolean containsAconfigChanges(Properties properties) {
+ for (String namespaceAndFlag : properties.getKeyset()) {
+ int firstStarIndex = namespaceAndFlag.indexOf("*");
+ if (firstStarIndex == -1 || firstStarIndex == 0
+ || firstStarIndex == namespaceAndFlag.length() - 1) {
+ Slog.w(TAG, "detected malformed staged flag: " + namespaceAndFlag);
+ continue;
+ }
+
+ String namespace = namespaceAndFlag.substring(0, firstStarIndex);
+ String flag = namespaceAndFlag.substring(firstStarIndex + 1);
+
+ if (aconfigFlags.get(namespace) != null && aconfigFlags.get(namespace).contains(flag)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private class PostNotificationBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
@@ -114,11 +145,15 @@ class BootNotificationCreator implements OnPropertiesChangedListener {
try {
Context resourcesContext = context.createPackageContext(RESOURCES_PACKAGE, 0);
+ Action action = new Action.Builder(
+ Icon.createWithResource(resourcesContext, R.drawable.ic_restart),
+ resourcesContext.getString(R.string.boot_notification_action_text),
+ pendingIntent).build();
Notification notification = new Notification.Builder(context, CHANNEL_ID)
.setContentText(resourcesContext.getString(R.string.boot_notification_content))
.setContentTitle(resourcesContext.getString(R.string.boot_notification_title))
.setSmallIcon(Icon.createWithResource(resourcesContext, R.drawable.ic_flag))
- .setContentIntent(pendingIntent)
+ .addAction(action)
.build();
notificationManager.notify(NOTIFICATION_ID, notification);
} catch (NameNotFoundException e) {
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
index 42cee70..b1ed0d6 100644
--- a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
+++ b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
@@ -1,36 +1,46 @@
package com.android.server.deviceconfig;
+import static com.android.server.deviceconfig.Flags.enableRebootNotification;
+import static com.android.server.deviceconfig.Flags.enableUnattendedReboot;
+
+import java.io.IOException;
import java.io.FileDescriptor;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.util.HashSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import android.aconfig.Aconfig.parsed_flags;
+import android.aconfig.Aconfig.parsed_flag;
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 SYSTEM_FLAGS_PATH = "/system/etc/aconfig_flags.pb";
+ private static final String SYSTEM_EXT_FLAGS_PATH = "/system_ext/etc/aconfig_flags.pb";
+ private static final String VENDOR_FLAGS_PATH = "/vendor/etc/aconfig_flags.pb";
+
private DeviceConfigInit() {
// do not instantiate
}
@@ -39,6 +49,7 @@ public class DeviceConfigInit {
@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)
@@ -59,10 +70,52 @@ public class DeviceConfigInit {
@Override
public void onStart() {
if (enableRebootNotification()) {
+ Map<String, Set<String>> aconfigFlags = new HashMap<>();
+ try {
+ addAconfigFlagsFromFile(aconfigFlags, SYSTEM_FLAGS_PATH);
+ addAconfigFlagsFromFile(aconfigFlags, SYSTEM_EXT_FLAGS_PATH);
+ addAconfigFlagsFromFile(aconfigFlags, VENDOR_FLAGS_PATH);
+ } catch (IOException e) {
+ Slog.e(TAG, "error loading aconfig flags", e);
+ }
+
+ BootNotificationCreator notifCreator = new BootNotificationCreator(
+ getContext().getApplicationContext(),
+ aconfigFlags);
+
DeviceConfig.addOnPropertiesChangedListener(
STAGED_NAMESPACE,
AsyncTask.THREAD_POOL_EXECUTOR,
- new BootNotificationCreator(getContext().getApplicationContext()));
+ notifCreator);
+ }
+
+ 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 addAconfigFlagsFromFile(Map<String, Set<String>> aconfigFlags,
+ String fileName) throws IOException {
+ byte[] contents = (new FileInputStream(fileName)).readAllBytes();
+ parsed_flags parsedFlags = parsed_flags.parseFrom(contents);
+ for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
+ if (aconfigFlags.get(flag.getNamespace()) == null) {
+ aconfigFlags.put(flag.getNamespace(), new HashSet<>());
+ aconfigFlags.get(flag.getNamespace()).add(flag.getName());
+ } else {
+ aconfigFlags.get(flag.getNamespace()).add(flag.getName());
+ }
}
}
@@ -80,5 +133,4 @@ public class DeviceConfigInit {
}
}
}
-
}
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/BootNotificationCreatorTest.java b/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
index 394864d..40e807a 100644
--- a/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
+++ b/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
@@ -16,33 +16,33 @@
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.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.anyInt;
-import static org.mockito.Mockito.anyLong;
import android.content.Context;
-import java.util.HashMap;
-
+import android.app.AlarmManager;
import android.content.ContextWrapper;
-import org.mockito.Mockito;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
-
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import android.app.AlarmManager;
-
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 {
@@ -64,17 +64,33 @@ public class BootNotificationCreatorTest {
}
}
};
- bootNotificationCreator = new BootNotificationCreator(mockContext);
+
+ 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 testNotificationScheduledWhenFlagStaged() {
+ public void testNotificationScheduledWhenAconfigFlagStaged() {
HashMap<String, String> flags = new HashMap();
- flags.put("test", "flag");
+ 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/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;
+ }
+ }
+ }
+}