diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-10-19 01:27:32 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-10-19 01:27:32 +0000 |
commit | 21b1512869af7ae91ec86c550df7ecf4a39a92af (patch) | |
tree | e2b02def2954c867ab4792974acac49c17c4ddf2 | |
parent | dff4aabb1286e60e2acc4f7a340ee4a142b3f7c3 (diff) | |
parent | b6487df1597b46b204d25b3498825dde13c09427 (diff) | |
download | ConfigInfrastructure-21b1512869af7ae91ec86c550df7ecf4a39a92af.tar.gz |
Snap for 10970117 from b6487df1597b46b204d25b3498825dde13c09427 to 24Q1-release
Change-Id: I70b6adc8ec72c844071e337148866bdea5747b9b
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; + } + } + } +} |