summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2024-03-06 09:30:08 -0800
committerXin Li <delphij@google.com>2024-03-06 09:30:08 -0800
commitbaba2caabc212bf23e261a02b58703830e23b1ff (patch)
tree4cd02b4c6548bfcd4db3aafa09468612f31cbf20
parent4af146d89bb4ebcb721da15d55db2df8083e8ee3 (diff)
parent1f8c0a519e8b885f4a89f603bf8e5d24289b2e54 (diff)
downloadConfigInfrastructure-baba2caabc212bf23e261a02b58703830e23b1ff.tar.gz
Merge Android 14 QPR2 to AOSP mainHEADmastermain
Bug: 319669529 Merged-In: I6f4193a7a21630d874eb615c0c8ae5c295aa6227 Change-Id: Ia705359e7984526b97803dae6f7845088497007b
-rw-r--r--apex/Android.bp15
-rw-r--r--framework/Android.bp16
-rw-r--r--framework/api/module-lib-current.txt1
-rw-r--r--framework/api/module-lib-lint-baseline.txt3
-rw-r--r--framework/api/system-current.txt17
-rw-r--r--framework/api/system-lint-baseline.txt15
-rw-r--r--framework/jarjar-rules.txt1
-rw-r--r--framework/java/android/provider/DeviceConfig.java250
-rw-r--r--framework/java/android/provider/DeviceConfigDataStore.java61
-rw-r--r--framework/java/android/provider/DeviceConfigManager.java76
-rw-r--r--framework/java/android/provider/DeviceConfigServiceDataStore.java165
-rw-r--r--framework/java/android/provider/SettingsConfigDataStore.java105
-rw-r--r--framework/java/android/provider/WritableFlags.java5
-rw-r--r--framework/java/android/provider/aidl/IDeviceConfigManager.aidl18
-rw-r--r--service/Android.bp28
-rw-r--r--service/ServiceResources/Android.bp36
-rw-r--r--service/ServiceResources/AndroidManifest.xml27
-rw-r--r--service/ServiceResources/res/drawable/ic_flag.xml10
-rw-r--r--service/ServiceResources/res/drawable/ic_restart.xml26
-rw-r--r--service/ServiceResources/res/values/strings.xml9
-rw-r--r--service/flags.aconfig21
-rw-r--r--service/jarjar-rules.txt5
-rw-r--r--service/java/com/android/server/deviceconfig/BootNotificationCreator.java231
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java126
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigInit.java105
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java99
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigShellService.java40
-rw-r--r--service/java/com/android/server/deviceconfig/SimPinReplayManager.java144
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManager.java323
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java50
-rw-r--r--service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java150
-rw-r--r--service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java84
-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
40 files changed, 3071 insertions, 77 deletions
diff --git a/apex/Android.bp b/apex/Android.bp
index 4aa79a8..abed18f 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -32,6 +32,17 @@ bootclasspath_fragment {
split_packages: [
"android.provider",
],
+
+ // The following packages and all their subpackages currently only
+ // contain classes from this bootclasspath_fragment. Listing a package
+ // here won't prevent other bootclasspath modules from adding classes in
+ // any of those packages but it will prevent them from adding those
+ // classes into an API surface, e.g. public, system, etc.. Doing so will
+ // result in a build failure due to inconsistent flags.
+ package_prefixes: [
+ "android.provider.aidl",
+ "android.provider.internal.modules.utils.build",
+ ],
},
// The bootclasspath_fragments that provide APIs on which this depends.
fragments: [
@@ -80,8 +91,12 @@ apex {
prebuilts: [
"current_sdkinfo",
],
+ min_sdk_version: "34",
key: "com.android.configinfrastructure.key",
certificate: ":com.android.configinfrastructure.certificate",
+ apps: [
+ "DeviceConfigServiceResources",
+ ],
}
sdk {
diff --git a/framework/Android.bp b/framework/Android.bp
index 2629228..efef3cc 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -18,17 +18,27 @@ package {
java_sdk_library {
name: "framework-configinfrastructure",
- srcs: ["java/**/*.java"],
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
defaults: ["framework-module-defaults"],
- permitted_packages: ["android.provider"],
+ permitted_packages: [
+ "android.provider",
+ "android.provider.aidl",
+ ],
apex_available: [
"com.android.configinfrastructure",
],
- min_sdk_version: "UpsideDownCake",
+ min_sdk_version: "34",
sdk_version: "module_current",
impl_library_visibility: [
"//packages/modules/ConfigInfrastructure:__subpackages__",
],
+ static_libs: [
+ "modules-utils-build",
+ ],
+ jarjar_rules: "jarjar-rules.txt",
lint: {
baseline_filename: "lint-baseline.xml",
},
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 739f954..ad11041 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -25,6 +25,7 @@ package android.provider {
field public static final String NAMESPACE_SETTINGS_STATS = "settings_stats";
field public static final String NAMESPACE_SETTINGS_UI = "settings_ui";
field public static final String NAMESPACE_TARE = "tare";
+ field public static final String NAMESPACE_TETHERING_NATIVE = "tethering_u_or_later_native";
field public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native";
field public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT = "vendor_system_native_boot";
field public static final String NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE = "virtualization_framework_native";
diff --git a/framework/api/module-lib-lint-baseline.txt b/framework/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..feb6f83
--- /dev/null
+++ b/framework/api/module-lib-lint-baseline.txt
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+UnflaggedApi: android.provider.DeviceConfig#NAMESPACE_TETHERING_NATIVE:
+ New API must be flagged with @FlaggedApi: field android.provider.DeviceConfig.NAMESPACE_TETHERING_NATIVE
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index e4154c6..a69e7fd 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -3,20 +3,25 @@ package android.provider {
public final class DeviceConfig {
method public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
+ method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void clearAllLocalOverrides();
+ method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void clearLocalOverride(@NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.MONITOR_DEVICE_CONFIG_ACCESS) public static void clearMonitorCallback(@NonNull android.content.ContentResolver);
method @RequiresPermission(anyOf={android.Manifest.permission.WRITE_DEVICE_CONFIG, android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG}) public static boolean deleteProperty(@NonNull String, @NonNull String);
method @NonNull public static java.util.Set<java.lang.String> getAdbWritableFlags();
+ method @NonNull public static java.util.Set<android.provider.DeviceConfig.Properties> getAllProperties();
method public static boolean getBoolean(@NonNull String, @NonNull String, boolean);
- method public static float getFloat(@NonNull String, @NonNull String, float);
- method public static int getInt(@NonNull String, @NonNull String, int);
- method public static long getLong(@NonNull String, @NonNull String, long);
- method @NonNull public static android.provider.DeviceConfig.Properties getProperties(@NonNull String, @NonNull java.lang.String...);
+ method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static float getFloat(@NonNull String, @NonNull String, float);
+ method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static int getInt(@NonNull String, @NonNull String, int);
+ method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static long getLong(@NonNull String, @NonNull String, long);
+ method @NonNull @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static android.provider.DeviceConfig.Properties getProperties(@NonNull String, @NonNull java.lang.String...);
method @Nullable public static String getProperty(@NonNull String, @NonNull String);
method @NonNull public static java.util.List<java.lang.String> getPublicNamespaces();
- method @Nullable public static String getString(@NonNull String, @NonNull String, @Nullable String);
+ method @Nullable @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getString(@NonNull String, @NonNull String, @Nullable String);
method @RequiresPermission(anyOf={android.Manifest.permission.WRITE_DEVICE_CONFIG, android.Manifest.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG}) public static int getSyncDisabledMode();
+ method @NonNull public static java.util.Map<java.lang.String,java.util.Map<java.lang.String,java.lang.String>> getUnderlyingValuesForOverriddenFlags();
method public static void removeOnPropertiesChangedListener(@NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
method @RequiresPermission(anyOf={android.Manifest.permission.WRITE_DEVICE_CONFIG, android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG}) public static void resetToDefaults(int, @Nullable String);
+ method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setLocalOverride(@NonNull String, @NonNull String, @NonNull String);
method @RequiresPermission(android.Manifest.permission.MONITOR_DEVICE_CONFIG_ACCESS) public static void setMonitorCallback(@NonNull android.content.ContentResolver, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.MonitorCallback);
method @RequiresPermission(anyOf={android.Manifest.permission.WRITE_DEVICE_CONFIG, android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG}) public static boolean setProperties(@NonNull android.provider.DeviceConfig.Properties) throws android.provider.DeviceConfig.BadConfigException;
method @RequiresPermission(anyOf={android.Manifest.permission.WRITE_DEVICE_CONFIG, android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG}) public static boolean setProperty(@NonNull String, @NonNull String, @Nullable String, boolean);
@@ -46,6 +51,7 @@ package android.provider {
field public static final String NAMESPACE_CONNECTIVITY = "connectivity";
field public static final String NAMESPACE_CONSTRAIN_DISPLAY_APIS = "constrain_display_apis";
field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture";
+ field public static final String NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL = "core_experiments_team_internal";
field public static final String NAMESPACE_CREDENTIAL = "credential_manager";
field @Deprecated public static final String NAMESPACE_DEX_BOOT = "dex_boot";
field public static final String NAMESPACE_DISPLAY_MANAGER = "display_manager";
@@ -63,6 +69,7 @@ package android.provider {
field public static final String NAMESPACE_MEDIA_NATIVE = "media_native";
field public static final String NAMESPACE_NEARBY = "nearby";
field public static final String NAMESPACE_NETD_NATIVE = "netd_native";
+ field public static final String NAMESPACE_NFC = "nfc";
field public static final String NAMESPACE_NNAPI_NATIVE = "nnapi_native";
field public static final String NAMESPACE_ON_DEVICE_PERSONALIZATION = "on_device_personalization";
field public static final String NAMESPACE_OTA = "ota";
diff --git a/framework/api/system-lint-baseline.txt b/framework/api/system-lint-baseline.txt
new file mode 100644
index 0000000..5cb0f45
--- /dev/null
+++ b/framework/api/system-lint-baseline.txt
@@ -0,0 +1,15 @@
+// Baseline format: 1.0
+UnflaggedApi: android.provider.DeviceConfig#NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL:
+ New API must be flagged with @FlaggedApi: field android.provider.DeviceConfig.NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL
+UnflaggedApi: android.provider.DeviceConfig#NAMESPACE_NFC:
+ New API must be flagged with @FlaggedApi: field android.provider.DeviceConfig.NAMESPACE_NFC
+UnflaggedApi: android.provider.DeviceConfig#clearAllLocalOverrides():
+ New API must be flagged with @FlaggedApi: method android.provider.DeviceConfig.clearAllLocalOverrides()
+UnflaggedApi: android.provider.DeviceConfig#clearLocalOverride(String, String):
+ New API must be flagged with @FlaggedApi: method android.provider.DeviceConfig.clearLocalOverride(String,String)
+UnflaggedApi: android.provider.DeviceConfig#getAllProperties():
+ New API must be flagged with @FlaggedApi: method android.provider.DeviceConfig.getAllProperties()
+UnflaggedApi: android.provider.DeviceConfig#getUnderlyingValuesForOverriddenFlags():
+ New API must be flagged with @FlaggedApi: method android.provider.DeviceConfig.getUnderlyingValuesForOverriddenFlags()
+UnflaggedApi: android.provider.DeviceConfig#setLocalOverride(String, String, String):
+ New API must be flagged with @FlaggedApi: method android.provider.DeviceConfig.setLocalOverride(String,String,String)
diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt
new file mode 100644
index 0000000..c0fbba5
--- /dev/null
+++ b/framework/jarjar-rules.txt
@@ -0,0 +1 @@
+rule com.android.modules.utils.** android.provider.internal.modules.utils.@1
diff --git a/framework/java/android/provider/DeviceConfig.java b/framework/java/android/provider/DeviceConfig.java
index 06d5572..6f169ac 100644
--- a/framework/java/android/provider/DeviceConfig.java
+++ b/framework/java/android/provider/DeviceConfig.java
@@ -17,6 +17,7 @@
package android.provider;
import static android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.Manifest.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG;
@@ -31,6 +32,7 @@ import android.annotation.SystemApi;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
+import com.android.modules.utils.build.SdkLevel;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
@@ -45,12 +47,20 @@ import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
+import android.util.Log;
+
+import android.provider.aidl.IDeviceConfigManager;
+import android.provider.DeviceConfigServiceManager;
+import android.provider.DeviceConfigInitializer;
+import android.os.IBinder;
+
/**
* Device level configuration parameters which can be tuned by a separate configuration service.
* Namespaces that end in "_native" such as {@link #NAMESPACE_NETD_NATIVE} are intended to be used
@@ -60,6 +70,14 @@ import java.util.concurrent.Executor;
*/
@SystemApi
public final class DeviceConfig {
+
+ /**
+ * The name of the service that provides the logic to these APIs
+ *
+ * @hide
+ */
+ public static final String SERVICE_NAME = "device_config_updatable";
+
/**
* Namespace for all accessibility related features.
*
@@ -199,6 +217,14 @@ public final class DeviceConfig {
public static final String NAMESPACE_BLUETOOTH = "bluetooth";
/**
+ * Namespace for features relating to android core experiments team internal usage.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String NAMESPACE_CORE_EXPERIMENTS_TEAM_INTERNAL = "core_experiments_team_internal";
+
+ /**
* Namespace for all camera-related features that are used at the native level.
*
* @hide
@@ -938,6 +964,27 @@ public final class DeviceConfig {
@SystemApi
public static final String NAMESPACE_REMOTE_AUTH = "remote_auth";
+
+ /**
+ * Namespace for tethering module native features.
+ * Flags defined in this namespace are only usable on
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and newer.
+ * On older Android releases, they will not be propagated to native code.
+ *
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static final String NAMESPACE_TETHERING_NATIVE =
+ "tethering_u_or_later_native";
+
+ /**
+ * Namespace for all near field communication (nfc) related features.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String NAMESPACE_NFC = "nfc";
+
/**
* The modes that can be used when disabling syncs to the 'config' settings.
* @hide
@@ -974,7 +1021,7 @@ public final class DeviceConfig {
*/
@SystemApi
public static final int SYNC_DISABLED_MODE_UNTIL_REBOOT = 2;
-
+
private static final Object sLock = new Object();
@GuardedBy("sLock")
private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
@@ -983,6 +1030,11 @@ public final class DeviceConfig {
private static Map<String, Pair<ContentObserver, Integer>> sNamespaces = new HashMap<>();
private static final String TAG = "DeviceConfig";
+ private static final DeviceConfigDataStore sDataStore = new SettingsConfigDataStore();
+
+ private static final String DEVICE_CONFIG_OVERRIDES_NAMESPACE =
+ "device_config_overrides";
+
/**
* Interface for monitoring callback functions.
*
@@ -1043,6 +1095,8 @@ public final class DeviceConfig {
* Each call to {@link #setProperties(Properties)} is also atomic and ensures that either none
* or all of the change is picked up here, but never only part of it.
*
+ * If there are any local overrides applied, they will take precedence over underlying values.
+ *
* @param namespace The namespace containing the properties to look up.
* @param names The names of properties to look up, or empty to fetch all properties for the
* given namespace.
@@ -1056,9 +1110,80 @@ public final class DeviceConfig {
*/
@SystemApi
@NonNull
- public static Properties getProperties(@NonNull String namespace, @NonNull String ... names) {
- return new Properties(namespace,
- Settings.Config.getStrings(namespace, Arrays.asList(names)));
+ @RequiresPermission(READ_DEVICE_CONFIG)
+ public static Properties getProperties(@NonNull String namespace, @NonNull String... names) {
+ Properties propertiesWithoutOverrides =
+ getPropertiesWithoutOverrides(namespace, names);
+ if (SdkLevel.isAtLeastV()) {
+ return applyOverrides(propertiesWithoutOverrides);
+ } else {
+ return propertiesWithoutOverrides;
+ }
+ }
+
+ @NonNull
+ private static Properties getPropertiesWithoutOverrides(@NonNull String namespace,
+ @NonNull String... names) {
+ return sDataStore.getProperties(namespace, names);
+ }
+
+ private static Properties applyOverrides(@NonNull Properties properties) {
+ Properties overrides =
+ getPropertiesWithoutOverrides(DEVICE_CONFIG_OVERRIDES_NAMESPACE);
+ Map<String, String> newPropertiesMap = new HashMap<>();
+
+ HashSet<String> flags = new HashSet(properties.getKeyset());
+ for (String override : overrides.getKeyset()) {
+ String[] namespaceAndFlag = override.split(":");
+ if (properties.getNamespace().equals(namespaceAndFlag[0])) {
+ flags.add(namespaceAndFlag[1]);
+ }
+ }
+
+ for (String flag : flags) {
+ String override =
+ overrides.getString(properties.getNamespace() + ":" + flag, null);
+ if (override != null) {
+ newPropertiesMap.put(flag, override);
+ } else {
+ newPropertiesMap.put(flag, properties.getString(flag, null));
+ }
+ }
+ return new Properties(properties.getNamespace(), newPropertiesMap);
+ }
+
+ /**
+ * List all stored flags.
+ *
+ * The keys take the form {@code namespace/name}, and the values are the flag values.
+ *
+ * @hide
+ */
+ @SystemApi
+ @NonNull
+ public static Set<Properties> getAllProperties() {
+ Map<String, String> properties = sDataStore.getAllProperties();
+ Map<String, Map<String, String>> propertyMaps = new HashMap<>();
+ for (String flag : properties.keySet()) {
+ String[] namespaceAndFlag = flag.split("/");
+ String namespace = namespaceAndFlag[0];
+ String flagName = namespaceAndFlag[1];
+ String override =
+ getProperty(DEVICE_CONFIG_OVERRIDES_NAMESPACE, namespace + ":" + flagName);
+
+ String value = override != null ? override : properties.get(flag);
+
+ if (!propertyMaps.containsKey(namespace)) {
+ propertyMaps.put(namespace, new HashMap<>());
+ }
+ propertyMaps.get(namespace).put(flagName, value);
+ }
+
+ HashSet<Properties> result = new HashSet<>();
+ for (Map.Entry<String, Map<String, String>> entry : propertyMaps.entrySet()) {
+ result.add(new Properties(entry.getKey(), entry.getValue()));
+ }
+ return result;
}
/**
@@ -1072,6 +1197,7 @@ public final class DeviceConfig {
* @hide
*/
@SystemApi
+ @RequiresPermission(READ_DEVICE_CONFIG)
@Nullable
public static String getString(@NonNull String namespace, @NonNull String name,
@Nullable String defaultValue) {
@@ -1107,6 +1233,7 @@ public final class DeviceConfig {
* @hide
*/
@SystemApi
+ @RequiresPermission(READ_DEVICE_CONFIG)
public static int getInt(@NonNull String namespace, @NonNull String name, int defaultValue) {
String value = getProperty(namespace, name);
if (value == null) {
@@ -1131,6 +1258,7 @@ public final class DeviceConfig {
* @hide
*/
@SystemApi
+ @RequiresPermission(READ_DEVICE_CONFIG)
public static long getLong(@NonNull String namespace, @NonNull String name, long defaultValue) {
String value = getProperty(namespace, name);
if (value == null) {
@@ -1155,6 +1283,7 @@ public final class DeviceConfig {
* @hide
*/
@SystemApi
+ @RequiresPermission(READ_DEVICE_CONFIG)
public static float getFloat(@NonNull String namespace, @NonNull String name,
float defaultValue) {
String value = getProperty(namespace, name);
@@ -1170,6 +1299,82 @@ public final class DeviceConfig {
}
/**
+ * Set flag {@code namespace/name} to {@code value}, and ignores server-updates for this flag.
+ *
+ * Can still be called even if there is no underlying value set.
+ *
+ * Returns {@code true} if successful, or {@code false} if the storage implementation throws
+ * errors.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_DEVICE_CONFIG)
+ public static boolean setLocalOverride(@NonNull String namespace, @NonNull String name,
+ @NonNull String value) {
+ return setProperty(DEVICE_CONFIG_OVERRIDES_NAMESPACE, namespace + ":" + name, value, false);
+ }
+
+ /**
+ * Clear all local sticky overrides.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_DEVICE_CONFIG)
+ public static void clearAllLocalOverrides() {
+ Properties overrides = getProperties(DEVICE_CONFIG_OVERRIDES_NAMESPACE);
+ for (String overrideName : overrides.getKeyset()) {
+ deleteProperty(DEVICE_CONFIG_OVERRIDES_NAMESPACE, overrideName);
+ }
+ }
+
+ /**
+ * Clear local sticky override for flag {@code namespace/name}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_DEVICE_CONFIG)
+ public static void clearLocalOverride(@NonNull String namespace,
+ @NonNull String name) {
+ deleteProperty(DEVICE_CONFIG_OVERRIDES_NAMESPACE, namespace + ":" + name);
+ }
+
+ /**
+ * Return a map containing all flags that have been overridden.
+ *
+ * The keys of the outer map are namespaces. They keys of the inner maps are
+ * flag names. The values of the inner maps are the underlying flag values
+ * (not to be confused with their overridden values).
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public static Map<String, Map<String, String>> getUnderlyingValuesForOverriddenFlags() {
+ Properties overrides = getProperties(DEVICE_CONFIG_OVERRIDES_NAMESPACE);
+ HashMap<String, Map<String, String>> result = new HashMap<>();
+ for (Map.Entry<String, String> entry : overrides.getPropertyValues().entrySet()) {
+ String[] namespaceAndFlag = entry.getKey().split(":");
+ String namespace = namespaceAndFlag[0];
+ String flag = namespaceAndFlag[1];
+
+ String actualValue =
+ getPropertiesWithoutOverrides(namespace, flag)
+ .getString(flag, null);
+ if (result.get(namespace) != null) {
+ result.get(namespace).put(flag, actualValue);
+ } else {
+ HashMap<String, String> innerMap = new HashMap<>();
+ innerMap.put(flag, actualValue);
+ result.put(namespace, innerMap);
+ }
+ }
+ return result;
+ }
+
+ /**
* Create a new property with the provided name and value in the provided namespace, or
* update the value of such a property if it already exists. The same name can exist in multiple
* namespaces and might have different values in any or all namespaces.
@@ -1193,7 +1398,7 @@ public final class DeviceConfig {
@RequiresPermission(anyOf = {WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG})
public static boolean setProperty(@NonNull String namespace, @NonNull String name,
@Nullable String value, boolean makeDefault) {
- return Settings.Config.putString(namespace, name, value, makeDefault);
+ return sDataStore.setProperty(namespace, name, value, makeDefault);
}
/**
@@ -1214,8 +1419,7 @@ public final class DeviceConfig {
@SystemApi
@RequiresPermission(anyOf = {WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG})
public static boolean setProperties(@NonNull Properties properties) throws BadConfigException {
- return Settings.Config.setStrings(properties.getNamespace(),
- properties.mMap);
+ return sDataStore.setProperties(properties);
}
/**
@@ -1230,7 +1434,7 @@ public final class DeviceConfig {
@SystemApi
@RequiresPermission(anyOf = {WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG})
public static boolean deleteProperty(@NonNull String namespace, @NonNull String name) {
- return Settings.Config.deleteString(namespace, name);
+ return sDataStore.deleteProperty(namespace, name);
}
/**
@@ -1261,7 +1465,7 @@ public final class DeviceConfig {
@SystemApi
@RequiresPermission(anyOf = {WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG})
public static void resetToDefaults(int resetMode, @Nullable String namespace) {
- Settings.Config.resetToDefaults(resetMode, namespace);
+ sDataStore.resetToDefaults(resetMode, namespace);
}
/**
@@ -1269,16 +1473,17 @@ public final class DeviceConfig {
* config values. This is intended for use during tests to prevent a sync operation clearing
* config values which could influence the outcome of the tests, i.e. by changing behavior.
*
- * @param syncDisabledMode the mode to use, see {@link #SYNC_DISABLED_MODE_NONE},
- * {@link #SYNC_DISABLED_MODE_PERSISTENT} and {@link #SYNC_DISABLED_MODE_UNTIL_REBOOT}
+ * @param syncDisabledMode the mode to use, see {@link Settings.Config#SYNC_DISABLED_MODE_NONE},
+ * {@link Settings.Config#SYNC_DISABLED_MODE_PERSISTENT} and {@link
+ * Settings.Config#SYNC_DISABLED_MODE_UNTIL_REBOOT}
*
* @see #getSyncDisabledMode()
* @hide
*/
@SystemApi
@RequiresPermission(anyOf = {WRITE_DEVICE_CONFIG, READ_WRITE_SYNC_DISABLED_MODE_CONFIG})
- public static void setSyncDisabledMode(@SyncDisabledMode int syncDisabledMode) {
- Settings.Config.setSyncDisabledMode(syncDisabledMode);
+ public static void setSyncDisabledMode(int syncDisabledMode) {
+ sDataStore.setSyncDisabledMode(syncDisabledMode);
}
/**
@@ -1290,7 +1495,7 @@ public final class DeviceConfig {
@SystemApi
@RequiresPermission(anyOf = {WRITE_DEVICE_CONFIG, READ_WRITE_SYNC_DISABLED_MODE_CONFIG})
public static int getSyncDisabledMode() {
- return Settings.Config.getSyncDisabledMode();
+ return sDataStore.getSyncDisabledMode();
}
/**
@@ -1364,7 +1569,7 @@ public final class DeviceConfig {
@NonNull ContentResolver resolver,
@NonNull @CallbackExecutor Executor executor,
@NonNull MonitorCallback callback) {
- Settings.Config.setMonitorCallback(resolver, executor, callback);
+ sDataStore.setMonitorCallback(resolver, executor, callback);
}
/**
@@ -1376,7 +1581,7 @@ public final class DeviceConfig {
@SystemApi
@RequiresPermission(Manifest.permission.MONITOR_DEVICE_CONFIG_ACCESS)
public static void clearMonitorCallback(@NonNull ContentResolver resolver) {
- Settings.Config.clearMonitorCallback(resolver);
+ sDataStore.clearMonitorCallback(resolver);
}
/**
@@ -1402,7 +1607,7 @@ public final class DeviceConfig {
}
}
};
- Settings.Config
+ sDataStore
.registerContentObserver(namespace, true, contentObserver);
sNamespaces.put(namespace, new Pair<>(contentObserver, 1));
}
@@ -1426,7 +1631,7 @@ public final class DeviceConfig {
sNamespaces.put(namespace, new Pair<>(namespaceCount.first, namespaceCount.second - 1));
} else {
// Decrementing a namespace to zero means we no longer need its ContentObserver.
- Settings.Config.unregisterContentObserver(namespaceCount.first);
+ sDataStore.unregisterContentObserver(namespaceCount.first);
sNamespaces.remove(namespace);
}
}
@@ -1655,6 +1860,15 @@ public final class DeviceConfig {
}
/**
+ * Returns a map with the underlying property values defined by this object
+ *
+ * @hide
+ */
+ public @NonNull Map<String, String> getPropertyValues() {
+ return new HashMap<>(mMap);
+ }
+
+ /**
* Builder class for the construction of {@link Properties} objects.
*/
public static final class Builder {
diff --git a/framework/java/android/provider/DeviceConfigDataStore.java b/framework/java/android/provider/DeviceConfigDataStore.java
new file mode 100644
index 0000000..bd9647c
--- /dev/null
+++ b/framework/java/android/provider/DeviceConfigDataStore.java
@@ -0,0 +1,61 @@
+/*
+ * 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 android.provider;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.provider.DeviceConfig;
+
+import java.util.concurrent.Executor;
+import java.util.Map;
+
+/**
+ * @hide
+ */
+public interface DeviceConfigDataStore {
+ @NonNull Map<String, String> getAllProperties();
+
+ @NonNull DeviceConfig.Properties getProperties(@NonNull String namespace, @NonNull String ... names);
+
+ boolean setProperties(@NonNull DeviceConfig.Properties properties) throws
+ DeviceConfig.BadConfigException;
+
+ boolean setProperty(@NonNull String namespace, @NonNull String name,
+ @Nullable String value, boolean makeDefault);
+
+ boolean deleteProperty(@NonNull String namespace, @NonNull String name);
+
+ void resetToDefaults(int resetMode, @Nullable String namespace);
+
+ void setSyncDisabledMode(int syncDisabledMode);
+ int getSyncDisabledMode();
+
+ void setMonitorCallback(
+ @NonNull ContentResolver resolver,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DeviceConfig.MonitorCallback callback);
+
+ void clearMonitorCallback(@NonNull ContentResolver resolver);
+
+ void registerContentObserver(@NonNull String namespace, boolean notifyForescendants,
+ ContentObserver contentObserver);
+
+ void unregisterContentObserver(@NonNull ContentObserver contentObserver);
+}
diff --git a/framework/java/android/provider/DeviceConfigManager.java b/framework/java/android/provider/DeviceConfigManager.java
new file mode 100644
index 0000000..3e50b8a
--- /dev/null
+++ b/framework/java/android/provider/DeviceConfigManager.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.provider;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import android.annotation.SystemService;
+import android.os.RemoteException;
+import android.provider.aidl.IDeviceConfigManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @hide
+ */
+@SystemService(DeviceConfig.SERVICE_NAME)
+public class DeviceConfigManager {
+
+ private IDeviceConfigManager mService;
+
+ public DeviceConfigManager(@NonNull IDeviceConfigManager service) {
+ mService = service;
+ }
+
+ @NonNull
+ public DeviceConfig.Properties getProperties(@NonNull String namespace,
+ @NonNull String... names) {
+ try {
+ Map<String, String> map = mService.getProperties(namespace, names);
+ return new DeviceConfig.Properties(namespace, map);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public boolean setProperties(@NonNull String namespace, @NonNull Map<String, String> values) {
+ try {
+ return mService.setProperties(namespace, values);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public boolean setProperty(@NonNull String namespace, @NonNull String name,
+ @Nullable String value, boolean makeDefault) {
+ try {
+ return mService.setProperty(namespace, name, value, makeDefault);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ public boolean deleteProperty(@NonNull String namespace, @NonNull String name) {
+ try {
+ return mService.deleteProperty(namespace, name);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/framework/java/android/provider/DeviceConfigServiceDataStore.java b/framework/java/android/provider/DeviceConfigServiceDataStore.java
new file mode 100644
index 0000000..cdcd5a8
--- /dev/null
+++ b/framework/java/android/provider/DeviceConfigServiceDataStore.java
@@ -0,0 +1,165 @@
+/*
+ * 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 android.provider;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.os.IBinder;
+import android.provider.aidl.IDeviceConfigManager;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public class DeviceConfigServiceDataStore /*implements DeviceConfigDataStore*/ {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "DeviceConfigServiceDataStore";
+ private static final Object mLock = new Object();
+
+ // TODO(b/265948914): finish implementation of this data store and make it actually implement
+ // the interface
+ private DeviceConfigManager mManager;
+
+ public DeviceConfigServiceDataStore() {
+
+ }
+
+ public DeviceConfigServiceDataStore(DeviceConfigManager deviceConfigManager) {
+ mManager = deviceConfigManager;
+ }
+
+ private @Nullable DeviceConfigManager createManagerIfNeeded() {
+ if (mManager != null) {
+ return mManager;
+ }
+ synchronized (mLock) {
+ if (mManager != null) {
+ return mManager;
+ }
+
+ IBinder binder = DeviceConfigInitializer.getDeviceConfigServiceManager()
+ .getDeviceConfigUpdatableServiceRegisterer()
+ .get();
+
+ if (binder != null) {
+ IDeviceConfigManager manager = IDeviceConfigManager.Stub.asInterface(
+ binder);
+ mManager = new DeviceConfigManager(manager);
+ }
+ return mManager;
+ }
+ }
+
+// @Override
+ @NonNull
+ public DeviceConfig.Properties getProperties(@NonNull String namespace,
+ @NonNull String... names) {
+ createManagerIfNeeded();
+
+ if (mManager == null) {
+ if(DEBUG) {
+ Slog.d(TAG, "ServiceDS - getProperties before ready " + namespace + " "
+ + Arrays.toString(names));
+ }
+ return new DeviceConfig.Properties(namespace, new HashMap<>());
+ }
+ return mManager.getProperties(namespace, names);
+ }
+
+// @Override
+ public boolean setProperties(@NonNull DeviceConfig.Properties properties)
+ throws DeviceConfig.BadConfigException {
+ createManagerIfNeeded();
+ if (mManager == null) {
+ if(DEBUG) {
+ Slog.d(TAG, "ServiceDS - setProperties before ready " + properties.getNamespace() + " " + properties);
+ }
+ return false;
+ }
+ return mManager.setProperties(properties.getNamespace(), properties.getPropertyValues());
+ }
+//
+// @Override
+ public boolean setProperty(@NonNull String namespace, @NonNull String name,
+ @Nullable String value, boolean makeDefault) {
+ createManagerIfNeeded();
+ if (mManager == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "ServiceDS - setProperty before ready " + namespace + " " + name);
+ }
+ return false;
+ }
+ return mManager.setProperty(namespace, name, value, makeDefault);
+ }
+//
+// @Override
+ public boolean deleteProperty(@NonNull String namespace, @NonNull String name) {
+ createManagerIfNeeded();
+ if (mManager == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "ServiceDS - setProperty before ready " + namespace + " " + name);
+ }
+ return false;
+ }
+ return mManager.deleteProperty(namespace, name);
+ }
+//
+// @Override
+ public void resetToDefaults(int resetMode, @Nullable String namespace) {
+// mManager.resetToDefaults(resetMode, namespace);
+ }
+//
+// @Override
+ public void setSyncDisabledMode(int syncDisabledMode) {
+// mManager.setSyncDisabledMode(syncDisabledMode);
+ }
+//
+// @Override
+ public int getSyncDisabledMode() {
+ // return mManager.getSyncDisabledMode();
+ return 0;
+ }
+//
+// @Override
+ public void setMonitorCallback(@NonNull ContentResolver resolver, @NonNull Executor executor,
+ @NonNull DeviceConfig.MonitorCallback callback) {
+// mManager.setMonitorCallback(resolver, executor, callback);
+ }
+//
+// @Override
+ public void clearMonitorCallback(@NonNull ContentResolver resolver) {
+// mManager.clearMonitorCallback(resolver);
+ }
+//
+// @Override
+ public void registerContentObserver(@NonNull String namespace, boolean notifyForDescendants,
+ ContentObserver contentObserver) {
+// mManager.registerContentObserver(namespace, notifyForescendants, contentObserver);
+ }
+//
+// @Override
+ public void unregisterContentObserver(@NonNull ContentObserver contentObserver) {
+// mManager.unregisterContentObserver(contentObserver);
+ }
+}
diff --git a/framework/java/android/provider/SettingsConfigDataStore.java b/framework/java/android/provider/SettingsConfigDataStore.java
new file mode 100644
index 0000000..33a880d
--- /dev/null
+++ b/framework/java/android/provider/SettingsConfigDataStore.java
@@ -0,0 +1,105 @@
+/*
+ * 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 android.provider;
+
+import android.content.ContentResolver;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * TODO: want to change the package of this class
+ *
+ * @hide
+ */
+public class SettingsConfigDataStore implements DeviceConfigDataStore {
+ @Override
+ public @NonNull Map<String, String> getAllProperties() {
+ return Settings.Config.getAllStrings();
+ }
+
+ @Override
+ public @NonNull DeviceConfig.Properties getProperties(@NonNull String namespace,
+ @NonNull String... names) {
+ return new DeviceConfig.Properties(namespace,
+ Settings.Config.getStrings(namespace, Arrays.asList(names)));
+ }
+
+ @Override
+ public boolean setProperties(@NonNull DeviceConfig.Properties properties)
+ throws DeviceConfig.BadConfigException {
+ return Settings.Config.setStrings(properties.getNamespace(),
+ properties.getPropertyValues());
+ }
+
+ @Override
+ public boolean setProperty(@NonNull String namespace, @NonNull String name,
+ @Nullable String value, boolean makeDefault) {
+ return Settings.Config.putString(namespace, name, value, makeDefault);
+ }
+
+ @Override
+ public boolean deleteProperty(@NonNull String namespace, @NonNull String name) {
+ return Settings.Config.deleteString(namespace, name);
+ }
+
+ @Override
+ public void resetToDefaults(int resetMode, @Nullable String namespace) {
+ Settings.Config.resetToDefaults(resetMode, namespace);
+ }
+
+ @Override
+ public void setSyncDisabledMode(int syncDisabledMode) {
+ Settings.Config.setSyncDisabledMode(syncDisabledMode);
+ }
+
+ @Override
+ public int getSyncDisabledMode() {
+ return Settings.Config.getSyncDisabledMode();
+ }
+
+ @Override
+ public void setMonitorCallback(@NonNull ContentResolver resolver, @NonNull Executor executor,
+ @NonNull DeviceConfig.MonitorCallback callback) {
+ Settings.Config.setMonitorCallback(resolver, executor, callback);
+ }
+
+ @Override
+ public void clearMonitorCallback(@NonNull ContentResolver resolver) {
+ Settings.Config.clearMonitorCallback(resolver);
+ }
+
+ @Override
+ public void registerContentObserver(@NonNull String namespace, boolean notifyForescendants,
+ ContentObserver contentObserver) {
+ Settings.Config.registerContentObserver(namespace, true, contentObserver);
+ }
+
+ @Override
+ public void unregisterContentObserver(@NonNull ContentObserver contentObserver) {
+ Settings.Config.unregisterContentObserver(contentObserver);
+ }
+}
diff --git a/framework/java/android/provider/WritableFlags.java b/framework/java/android/provider/WritableFlags.java
index b8c5b28..0219346 100644
--- a/framework/java/android/provider/WritableFlags.java
+++ b/framework/java/android/provider/WritableFlags.java
@@ -1460,10 +1460,8 @@ final class WritableFlags {
"privacy/camera_mic_icons_enabled",
"privacy/camera_toggle_enabled",
"privacy/data_sharing_update_period_millis",
- "privacy/location_access_check_enabled",
"privacy/location_access_check_delay_millis",
"privacy/location_access_check_periodic_interval_millis",
- "privacy/location_accuracy_enabled",
"privacy/location_indicators_enabled",
"privacy/location_indicators_show_system",
"privacy/location_indicators_small_enabled",
@@ -1475,7 +1473,6 @@ final class WritableFlags {
"privacy/param",
"privacy/param",
"privacy/permission_rationale_enabled",
- "privacy/permissions_hub_enabled",
"privacy/permissions_hub_subattribution_enabled",
"privacy/placeholder_safety_label_updates_enabled",
"privacy/privacy_attribution_tag_full_log_enabled",
@@ -1505,7 +1502,6 @@ final class WritableFlags {
"privacy/safety_label_changes_job_interval_millis",
"privacy/sc_accessibility_listener_enabled",
"privacy/sc_accessibility_source_enabled",
- "privacy/warning_banner_enabled",
"profcollect_native_boot/enable",
"remote_key_provisioning_native/enable_rkpd",
"rollback/containing",
@@ -1739,6 +1735,7 @@ final class WritableFlags {
"vpn/enable_pixel_ppn_feature",
"wallpaper_content/enabled",
"wearable_sensing/service_enabled_platforms",
+ "wearable_sensing/service_enabled",
"wear/ambient_auto_resume_timeout_max_reset_count",
"wear/bedtime_hard_mode_feature_enabled",
"wear/enable_backup_service_in_wear_framework",
diff --git a/framework/java/android/provider/aidl/IDeviceConfigManager.aidl b/framework/java/android/provider/aidl/IDeviceConfigManager.aidl
new file mode 100644
index 0000000..eef6e77
--- /dev/null
+++ b/framework/java/android/provider/aidl/IDeviceConfigManager.aidl
@@ -0,0 +1,18 @@
+package android.provider.aidl;
+
+/**
+ * {@hide}
+ */
+interface IDeviceConfigManager {
+// TODO(b/265948914): maybe rename this IDeviceConfigService ? ManagerService?
+
+ Map<String, String> getProperties(String namespace, in String[] names);
+
+ boolean setProperties(String namespace, in Map<String, String> values);
+
+ boolean setProperty(String namespace, String key, String value, boolean makeDefault);
+
+ boolean deleteProperty(String namespace, String key);
+
+ // TODO(b/265948914): add remaining methods
+}
diff --git a/service/Android.bp b/service/Android.bp
index 240fe5f..c837e2d 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -31,12 +31,40 @@ java_sdk_library {
"com.android.configinfrastructure",
],
static_libs: [
+ "modules-utils-build",
"modules-utils-shell-command-handler",
+ "device_config_reboot_flags_java_lib",
+ "guava",
+ "libaconfig_java_proto_lite"
],
libs: [
"framework-configinfrastructure.impl",
+ "framework-connectivity.stubs.module_lib",
+ ],
+ impl_only_libs: [
+ "DeviceConfigServiceResources",
],
min_sdk_version: "UpsideDownCake",
sdk_version: "system_server_current",
jarjar_rules: "jarjar-rules.txt",
+ impl_library_visibility: [
+ "//packages/modules/ConfigInfrastructure/service/javatests",
+ ],
+}
+
+aconfig_declarations {
+ name: "device_config_reboot_flags",
+ package: "com.android.server.deviceconfig",
+ srcs: [
+ "flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "device_config_reboot_flags_java_lib",
+ min_sdk_version: "UpsideDownCake",
+ apex_available: [
+ "com.android.configinfrastructure",
+ ],
+ aconfig_declarations: "device_config_reboot_flags",
}
diff --git a/service/ServiceResources/Android.bp b/service/ServiceResources/Android.bp
new file mode 100644
index 0000000..cd2dcc8
--- /dev/null
+++ b/service/ServiceResources/Android.bp
@@ -0,0 +1,36 @@
+//
+// Copyright (C) 2020 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.
+//
+
+// APK to hold all the wifi overlayable resources.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "DeviceConfigServiceResources",
+ package_name: "com.android.server.deviceconfig.resources",
+ sdk_version: "system_current",
+ resource_dirs: [
+ "res",
+ ],
+ certificate: "platform",
+ min_sdk_version: "34",
+ // platform_apis: true,
+ export_package_resources: true,
+ apex_available: [
+ "com.android.configinfrastructure",
+ ],
+}
diff --git a/service/ServiceResources/AndroidManifest.xml b/service/ServiceResources/AndroidManifest.xml
new file mode 100644
index 0000000..d4e2796
--- /dev/null
+++ b/service/ServiceResources/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?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 for resources APK -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.deviceconfig.resources"
+ coreApp="true"
+ android:versionCode="1"
+ android:versionName="V">
+ <application>
+ </application>
+</manifest>
diff --git a/service/ServiceResources/res/drawable/ic_flag.xml b/service/ServiceResources/res/drawable/ic_flag.xml
new file mode 100644
index 0000000..db86d5d
--- /dev/null
+++ b/service/ServiceResources/res/drawable/ic_flag.xml
@@ -0,0 +1,10 @@
+<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:attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M5,21V4H14L14.4,6H20V16H13L12.6,14H7V21ZM12.5,10ZM14.65,14H18V8H12.75L12.35,6H7V12H14.25Z"/>
+</vector>
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
new file mode 100644
index 0000000..ed60624
--- /dev/null
+++ b/service/ServiceResources/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- 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">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
new file mode 100644
index 0000000..e1396a0
--- /dev/null
+++ b/service/flags.aconfig
@@ -0,0 +1,21 @@
+package: "com.android.server.deviceconfig"
+
+flag {
+ name: "enable_reboot_notification"
+ namespace: "core_experiments_team_internal"
+ 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"
+}
+
+flag {
+ name: "enable_sim_pin_replay"
+ namespace: "core_experiments_team_internal"
+ description: "This flag controls enabling sim pin replay for unattended reboot."
+ bug: "305269414"
+}
diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt
index cf60d8f..c284d6e 100644
--- a/service/jarjar-rules.txt
+++ b/service/jarjar-rules.txt
@@ -1 +1,6 @@
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 com.google.common.** com.android.server.deviceconfig.internal.common.@1
+rule javax.annotation.** com.android.server.deviceconfig.javax.annotation.@1
+rule com.google.thirdparty.publicsuffix.** com.android.server.deviceconfig.publicsuffix.@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
new file mode 100644
index 0000000..d29f317
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java
@@ -0,0 +1,231 @@
+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;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.PowerManager;
+import android.provider.DeviceConfig.OnPropertiesChangedListener;
+import android.provider.DeviceConfig.Properties;
+import android.util.Slog;
+import com.android.server.deviceconfig.resources.R;
+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;
+import static java.time.temporal.ChronoUnit.SECONDS;
+
+/**
+ * Creates notifications when aconfig flags are staged on the device.
+ *
+ * The notification alerts the user to reboot, to apply the staged flags.
+ *
+ * @hide
+ */
+class BootNotificationCreator implements OnPropertiesChangedListener {
+ private static final String TAG = "DeviceConfigBootNotificationCreator";
+
+ private static final String RESOURCES_PACKAGE =
+ "com.android.server.deviceconfig.resources";
+
+ private static final String REBOOT_REASON = "DeviceConfig";
+
+ private static final String ACTION_TRIGGER_HARD_REBOOT =
+ "com.android.server.deviceconfig.TRIGGER_HARD_REBOOT";
+ private static final String ACTION_POST_NOTIFICATION =
+ "com.android.server.deviceconfig.POST_NOTIFICATION";
+
+ private static final String CHANNEL_ID = "trunk-stable-flags";
+ private static final String CHANNEL_NAME = "Trunkfood flags";
+ private static final int NOTIFICATION_ID = 111555;
+
+ private NotificationManager notificationManager;
+ private PowerManager powerManager;
+ private AlarmManager alarmManager;
+
+ private Context context;
+
+ private static final int REBOOT_HOUR = 10;
+ private static final int REBOOT_MINUTE = 0;
+ private static final int MIN_SECONDS_TO_SHOW_NOTIF = 86400;
+
+ private LocalDateTime lastReboot;
+
+ 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(),
+ new IntentFilter(ACTION_TRIGGER_HARD_REBOOT),
+ Context.RECEIVER_EXPORTED);
+ this.context.registerReceiver(
+ new PostNotificationBroadcastReceiver(),
+ new IntentFilter(ACTION_POST_NOTIFICATION),
+ Context.RECEIVER_EXPORTED);
+
+ this.lastReboot = LocalDateTime.now(ZoneId.systemDefault());
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!containsAconfigChanges(properties)) {
+ return;
+ }
+
+ if (!tryInitializeDependenciesIfNeeded()) {
+ Slog.i(TAG, "not posting notif; service dependencies not ready");
+ return;
+ }
+
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 1,
+ new Intent(ACTION_POST_NOTIFICATION),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ ZonedDateTime now = Instant
+ .ofEpochMilli(System.currentTimeMillis())
+ .atZone(ZoneId.systemDefault());
+
+ LocalDateTime currentTime = now.toLocalDateTime();
+ LocalDateTime postTime = now.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE);
+
+ LocalDateTime scheduledPostTime =
+ currentTime.isBefore(postTime) ? postTime : postTime.plusDays(1);
+ long scheduledPostTimeLong = scheduledPostTime
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli();
+
+ alarmManager.setExact(
+ 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) {
+ LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault());
+
+ if (lastReboot.until(now, SECONDS) < MIN_SECONDS_TO_SHOW_NOTIF) {
+ Slog.w(TAG, "not enough time passed, punting");
+ tryAgainIn24Hours(now);
+ return;
+ }
+
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 1,
+ new Intent(ACTION_TRIGGER_HARD_REBOOT),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ 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))
+ .addAction(action)
+ .build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ } catch (NameNotFoundException e) {
+ Slog.e(TAG, "failed to post boot notification", e);
+ }
+ }
+
+ private void tryAgainIn24Hours(LocalDateTime currentTime) {
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 1,
+ new Intent(ACTION_POST_NOTIFICATION),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ LocalDateTime postTime =
+ currentTime.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE).plusDays(1);
+ long scheduledPostTimeLong = postTime
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli();
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent);
+ }
+ }
+
+ private class HardRebootBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ powerManager.reboot(REBOOT_REASON);
+ }
+ }
+
+ /**
+ * If deps are not initialized yet, try to initialize them.
+ *
+ * @return true if the dependencies are newly or already initialized,
+ * or false if they are not ready yet
+ */
+ private boolean tryInitializeDependenciesIfNeeded() {
+ if (notificationManager == null) {
+ notificationManager = context.getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(
+ new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_HIGH));
+ }
+ }
+
+ if (alarmManager == null) {
+ alarmManager = context.getSystemService(AlarmManager.class);
+ }
+
+ if (powerManager == null) {
+ powerManager = context.getSystemService(PowerManager.class);
+ }
+
+ return notificationManager != null
+ && alarmManager != null
+ && powerManager != null;
+ }
+}
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java b/service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java
new file mode 100644
index 0000000..b480bc2
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java
@@ -0,0 +1,126 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.provider.DeviceConfig;
+import android.util.Slog;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+/**
+ * @hide
+ */
+public class DeviceConfigBootstrapValues {
+ private static final String TAG = "DeviceConfig";
+ private static final String SYSTEM_OVERRIDES_PATH = "file:///system/etc/device-config-defaults";
+ private static final String META_NAMESPACE = "DeviceConfigBootstrapValues";
+ private static final String META_KEY = "processed_values";
+
+ private final String defaultValuesPath;
+
+ public DeviceConfigBootstrapValues() {
+ this(SYSTEM_OVERRIDES_PATH);
+ }
+
+ public DeviceConfigBootstrapValues(String defaultValuesPath) {
+ this.defaultValuesPath = defaultValuesPath;
+ }
+
+ /**
+ * Performs the logic to apply bootstrap values when needed.
+ *
+ * If a file with the bootstrap values exists and they haven't been parsed before,
+ * it will parse the file and apply the values.
+ *
+ * @throws IOException if there's a problem reading the bootstrap file
+ * @throws RuntimeException if setting the values in DeviceConfig throws an exception
+ */
+ public void applyValuesIfNeeded() throws IOException {
+ if (getPath().toFile().exists()) {
+ if (checkIfHasAlreadyParsedBootstrapValues()) {
+ Slog.i(TAG, "Bootstrap values already parsed, not processing again");
+ } else {
+ parseAndApplyBootstrapValues();
+ Slog.i(TAG, "Parsed bootstrap values");
+ }
+ } else {
+ Slog.i(TAG, "Bootstrap values not found");
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private boolean checkIfHasAlreadyParsedBootstrapValues() {
+ DeviceConfig.Properties properties = DeviceConfig.getProperties(META_NAMESPACE);
+ return properties.getKeyset().size() > 0;
+ }
+
+ @SuppressLint("MissingPermission")
+ private void parseAndApplyBootstrapValues() throws IOException {
+ Path path = getPath();
+ try (Stream<String> lines = Files.lines(path)) {
+ lines.forEach(line -> processLine(line));
+ }
+ // store a property in DeviceConfig so that we know we have successufully
+ // processed this
+ writeToDeviceConfig(META_NAMESPACE, META_KEY, "true");
+ }
+
+ private void processLine(String line) {
+ // contents for each line:
+ // <namespace>:<package>.<flag-name>=[enabled|disabled]
+ // we actually use <package>.<flag-name> combined in calls into DeviceConfig
+ int namespaceDelimiter = line.indexOf(':');
+ String namespace = line.substring(0, namespaceDelimiter);
+ if (namespaceDelimiter < 1) {
+ throw new IllegalArgumentException("Unexpectedly found : at index "
+ + namespaceDelimiter);
+ }
+ int valueDelimiter = line.indexOf('=');
+ if (valueDelimiter < 5) {
+ throw new IllegalArgumentException("Unexpectedly found = at index " + valueDelimiter);
+ }
+ String key = line.substring(namespaceDelimiter + 1, valueDelimiter);
+ String value = line.substring(valueDelimiter + 1);
+ String val;
+ if ("enabled".equals(value)) {
+ val = "true";
+ } else if ("disabled".equals(value)) {
+ val = "false";
+ } else {
+ throw new IllegalArgumentException("Received unexpected value: " + value);
+ }
+ writeToDeviceConfig(namespace, key, val);
+ }
+
+ @SuppressLint("MissingPermission")
+ private void writeToDeviceConfig(String namespace, String key, String value) {
+ boolean result = DeviceConfig.setProperty(namespace, key, value, /* makeDefault= */ true);
+ if (!result) {
+ throw new RuntimeException("Failed to set DeviceConfig property [" + namespace + "] "
+ + key + "=" + value);
+ }
+ }
+
+ private Path getPath() {
+ return Path.of(URI.create(defaultValuesPath));
+ }
+}
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
index 0921d81..b1ed0d6 100644
--- a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
+++ b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
@@ -1,18 +1,45 @@
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.UpdatableDeviceConfigServiceReadiness;
-
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.content.ComponentName;
+import android.util.Slog;
+import com.android.modules.utils.build.SdkLevel;
import com.android.server.SystemService;
/** @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
@@ -21,24 +48,88 @@ public class DeviceConfigInit {
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public static class Lifecycle extends SystemService {
- private DeviceConfigShellService mShellService;
+ private DeviceConfigServiceImpl mService;
+ private UnattendedRebootManager mUnattendedRebootManager;
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public Lifecycle(@NonNull Context context) {
super(context);
- // this service is always instantiated but should only launch subsequent services
+ // this service is always instantiated but should only launch subsequent service(s)
// if the module is ready
if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) {
- mShellService = new DeviceConfigShellService();
+ mService = new DeviceConfigServiceImpl(getContext());
+ publishBinderService(DeviceConfig.SERVICE_NAME, mService);
}
+ applyBootstrapValues();
}
- /** @hide */
+ /**
+ * @hide
+ */
@Override
public void onStart() {
- if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) {
- publishBinderService("device_config_updatable", mShellService);
+ 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,
+ 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());
+ }
+ }
+ }
+
+ private void applyBootstrapValues() {
+ if (SdkLevel.isAtLeastV()) {
+ try {
+ new DeviceConfigBootstrapValues().applyValuesIfNeeded();
+ } catch (RuntimeException e) {
+ Slog.e(TAG, "Failed to load boot overrides", e);
+ throw e;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to load boot overrides", e);
+ throw new RuntimeException(e);
+ }
}
}
}
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java b/service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java
new file mode 100644
index 0000000..ddc69db
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java
@@ -0,0 +1,99 @@
+/*
+ * 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.annotation.NonNull;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.provider.aidl.IDeviceConfigManager;
+import android.provider.DeviceConfigInitializer;
+
+import com.android.server.deviceconfig.db.DeviceConfigDbAdapter;
+import com.android.server.deviceconfig.db.DeviceConfigDbHelper;
+
+import java.io.PrintWriter;
+import java.util.Map;
+
+import com.android.modules.utils.BasicShellCommandHandler;
+
+/**
+ * DeviceConfig Service implementation (updatable via Mainline) that uses a SQLite database as a storage mechanism
+ * for the configuration values.
+ *
+ * @hide
+ */
+public class DeviceConfigServiceImpl extends IDeviceConfigManager.Stub {
+ private final DeviceConfigDbAdapter mDbAdapter;
+
+ public DeviceConfigServiceImpl(Context context) {
+ DeviceConfigDbHelper dbHelper = new DeviceConfigDbHelper(context);
+ mDbAdapter = new DeviceConfigDbAdapter(dbHelper.getWritableDatabase());
+
+ DeviceConfigInitializer.getDeviceConfigServiceManager()
+ .getDeviceConfigUpdatableServiceRegisterer()
+ .register(this);
+ }
+
+ @Override
+ public Map<String, String> getProperties(String namespace, String[] names) throws RemoteException {
+ return mDbAdapter.getValuesForNamespace(namespace, names);
+ }
+
+ @Override
+ public boolean setProperties(String namespace, Map<String, String> values) {
+ return mDbAdapter.setValues(namespace, values);
+ }
+
+ @Override
+ public boolean setProperty(String namespace, String key, String value, boolean makeDefault) {
+ return mDbAdapter.setValue(namespace, key, value, makeDefault);
+ }
+
+ @Override
+ public boolean deleteProperty(String namespace, String key) {
+ return mDbAdapter.deleteValue(namespace, key);
+ }
+
+ @Override
+ public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ return (new MyShellCommand()).exec(
+ this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(),
+ args);
+ }
+
+ static final class MyShellCommand extends BasicShellCommandHandler {
+ // TODO(b/265948938) implement this
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) {
+ onHelp();
+ return -1;
+ }
+ return -1;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Device Config implemented in mainline");
+ }
+ }
+}
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigShellService.java b/service/java/com/android/server/deviceconfig/DeviceConfigShellService.java
deleted file mode 100644
index 14ced76..0000000
--- a/service/java/com/android/server/deviceconfig/DeviceConfigShellService.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.android.server.deviceconfig;
-
-import java.io.PrintWriter;
-
-import android.annotation.NonNull;
-import android.os.Binder;
-import android.os.ParcelFileDescriptor;
-
-import com.android.modules.utils.BasicShellCommandHandler;
-
-/** @hide */
-public class DeviceConfigShellService extends Binder {
-
- @Override
- public int handleShellCommand(@NonNull ParcelFileDescriptor in,
- @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
- @NonNull String[] args) {
- return (new MyShellCommand()).exec(
- this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(),
- args);
- }
-
- static final class MyShellCommand extends BasicShellCommandHandler {
-
- @Override
- public int onCommand(String cmd) {
- if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) {
- onHelp();
- return -1;
- }
- return -1;
- }
-
- @Override
- public void onHelp() {
- PrintWriter pw = getOutPrintWriter();
- pw.println("Device Config implemented in mainline");
- }
- }
-}
diff --git a/service/java/com/android/server/deviceconfig/SimPinReplayManager.java b/service/java/com/android/server/deviceconfig/SimPinReplayManager.java
new file mode 100644
index 0000000..93c3f5f
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/SimPinReplayManager.java
@@ -0,0 +1,144 @@
+package com.android.server.deviceconfig;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * If device contains a SIM PIN, must prepare <a
+ * href="https://source.android.com/docs/core/ota/resume-on-reboot#sim-pin-replay">Sim Pin
+ * Replay</a> to unlock the device post reboot.
+ *
+ * @hide
+ */
+public class SimPinReplayManager {
+
+ private static final String TAG = "UnattendedRebootManager";
+
+ // The identifier of the system resource value that determines whether auto-sim-unlock feature is
+ // enabled/disabled for the device.
+ private static final String SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY =
+ "config_allow_pin_storage_for_unattended_reboot";
+ // This is a copy of the hidden field
+ // CarrierConfigManager#KEY_STORE_SIM_PIN_FOR_UNATTENDED_REBOOT_BOOL. Phonesky uses this key to
+ // read the boolean value in carrier configs specifying whether to enable/disable auto-sim-unlock.
+ private static final String CARRIER_ENABLE_SIM_PIN_STORAGE_KEY =
+ "store_sim_pin_for_unattended_reboot_bool";
+
+ private Context mContext;
+
+ SimPinReplayManager(Context context) {
+ mContext = context;
+ }
+
+ /** Returns true, if no SIM PIN present or prepared SIM PIN Replay. */
+ public boolean prepareSimPinReplay() {
+ // Is SIM Pin present?
+ ImmutableList<Integer> pinLockedSubscriptionIds = getPinLockedSubscriptionIds(mContext);
+ if (pinLockedSubscriptionIds.isEmpty()) {
+ return true;
+ }
+
+ if (!isSimPinStorageEnabled(mContext, pinLockedSubscriptionIds)) {
+ Log.w(TAG, "SIM PIN storage is disabled");
+ return false;
+ }
+
+ TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+ if (telephonyManager == null) {
+ Log.e(TAG, "Failed to prepare SIM PIN Replay, TelephonyManager is null");
+ return false;
+ }
+
+ int prepareUnattendedRebootResult = telephonyManager.prepareForUnattendedReboot();
+ if (prepareUnattendedRebootResult == TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS) {
+ Log.i(TAG, "SIM PIN replay prepared");
+ return true;
+ }
+ Log.w(TAG, "Failed to prepare SIM PIN Replay, " + prepareUnattendedRebootResult);
+ return false;
+ }
+
+ /** Returns a list of telephony subscription IDs (SIM IDs) locked by PIN. */
+ private static ImmutableList<Integer> getPinLockedSubscriptionIds(Context context) {
+ SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class);
+ int[] subscriptionIds = subscriptionManager.getActiveSubscriptionIdList();
+ if (subscriptionIds.length == 0) {
+ return ImmutableList.of();
+ }
+
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ ImmutableList.Builder<Integer> pinLockedSubscriptionIdsBuilder = ImmutableList.builder();
+ for (int subscriptionId : subscriptionIds) {
+ if (telephonyManager.createForSubscriptionId(subscriptionId).isIccLockEnabled()) {
+ pinLockedSubscriptionIdsBuilder.add(subscriptionId);
+ }
+ }
+ return pinLockedSubscriptionIdsBuilder.build();
+ }
+
+ /**
+ * Returns true, if SIM PIN storage is enabled.
+ *
+ * <p>The SIM PIN storage might be disabled by OEM or by carrier, subscription (SIM) Id is
+ * required when checking if the corresponding SIM PIN storage is disabled by the carrier.
+ *
+ * <p>Both the OEM and carrier enable SIM PIN storage by default. If fails to read the OEM/carrier
+ * configs, it assume SIM PIN storage is enabled.
+ */
+ private static boolean isSimPinStorageEnabled(
+ Context context, ImmutableList<Integer> pinLockedSubscriptionIds) {
+ if (!isSystemEnableSimPin()) {
+ return false;
+ }
+
+ // If the carrier enables SIM PIN.
+ CarrierConfigManager carrierConfigManager =
+ context.getSystemService(CarrierConfigManager.class);
+ if (carrierConfigManager == null) {
+ Log.w(TAG, "CarrierConfigManager is null");
+ return true;
+ }
+ for (int pinLockedSubscriptionId : pinLockedSubscriptionIds) {
+ PersistableBundle subscriptionConfig =
+ carrierConfigManager.getConfigForSubId(
+ pinLockedSubscriptionId, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY);
+ // Only disable if carrier explicitly disables sim pin storage.
+ if (!subscriptionConfig.isEmpty()
+ && !subscriptionConfig.getBoolean(
+ CARRIER_ENABLE_SIM_PIN_STORAGE_KEY, /* defaultValue= */ true)) {
+ Log.w(
+ TAG,
+ "The carrier disables SIM PIN storage on subscription ID " + pinLockedSubscriptionId);
+ return false;
+ }
+ }
+ Log.v(TAG, "SIM PIN Storage is enabled");
+ return true;
+ }
+
+ private static boolean isSystemEnableSimPin() {
+ try {
+ boolean value =
+ Resources.getSystem()
+ .getBoolean(
+ Resources.getSystem()
+ .getIdentifier(
+ SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY,
+ /* defType= */ "bool",
+ /* defPackage= */ "android"));
+ Log.i(TAG, SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY + " = " + value);
+ return value;
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Could not read system resource value ," + SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY);
+ // When not explicitly disabled, assume SIM PIN storage functions properly.
+ return true;
+ }
+ }
+}
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..30f2439
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java
@@ -0,0 +1,323 @@
+package com.android.server.deviceconfig;
+
+import static com.android.server.deviceconfig.Flags.enableSimPinReplay;
+
+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.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.os.SystemClock;
+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;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 = 3;
+ private static final int DEFAULT_REBOOT_WINDOW_END_TIME_HOUR = 5;
+
+ private static final int DEFAULT_REBOOT_FREQUENCY_DAYS = 2;
+
+ private static final String TAG = "UnattendedRebootManager";
+
+ static final String REBOOT_REASON = "unattended,flaginfra";
+
+ @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 final SimPinReplayManager mSimPinReplayManager;
+
+ private static class InjectorImpl implements UnattendedRebootManagerInjector {
+ InjectorImpl() {
+ /*no op*/
+ }
+
+ public long now() {
+ return System.currentTimeMillis();
+ }
+
+ public ZoneId zoneId() {
+ return ZoneId.systemDefault();
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ public int getRebootStartTime() {
+ return DEFAULT_REBOOT_WINDOW_START_TIME_HOUR;
+ }
+
+ public int getRebootEndTime() {
+ return DEFAULT_REBOOT_WINDOW_END_TIME_HOUR;
+ }
+
+ public int getRebootFrequency() {
+ return DEFAULT_REBOOT_FREQUENCY_DAYS;
+ }
+
+ public void setRebootAlarm(Context context, long rebootTimeMillis) {
+ AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP, rebootTimeMillis, createTriggerRebootPendingIntent(context));
+ }
+
+ public void triggerRebootOnNetworkAvailable(Context context) {
+ final ConnectivityManager connectivityManager =
+ context.getSystemService(ConnectivityManager.class);
+ NetworkRequest request =
+ new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build();
+ connectivityManager.requestNetwork(request, createTriggerRebootPendingIntent(context));
+ }
+
+ 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);
+ }
+
+ private static PendingIntent createTriggerRebootPendingIntent(Context context) {
+ return PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 0,
+ new Intent(ACTION_TRIGGER_REBOOT),
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+ }
+ }
+
+ @VisibleForTesting
+ UnattendedRebootManager(
+ Context context,
+ UnattendedRebootManagerInjector injector,
+ SimPinReplayManager simPinReplayManager) {
+ mContext = context;
+ mInjector = injector;
+ mSimPinReplayManager = simPinReplayManager;
+
+ 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(), new SimPinReplayManager(context));
+ }
+
+ 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= */ 12);
+ 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() {
+ Log.v(TAG, "Attempting unattended reboot");
+
+ // Has enough time passed since reboot?
+ if (TimeUnit.MILLISECONDS.toDays(mInjector.elapsedRealtime())
+ < mInjector.getRebootFrequency()) {
+ Log.v(
+ TAG,
+ "Device has already been rebooted in that last "
+ + mInjector.getRebootFrequency()
+ + " days.");
+ scheduleReboot();
+ return;
+ }
+ // Is RoR is supported?
+ if (!isDeviceSecure(mContext)) {
+ Log.v(TAG, "Device is not secure. Proceed with regular reboot");
+ mInjector.regularReboot(mContext);
+ return;
+ }
+ // Is RoR prepared?
+ if (!isPreparedForUnattendedReboot()) {
+ Log.v(TAG, "Lskf is not captured, reschedule reboot.");
+ prepareUnattendedReboot();
+ scheduleReboot();
+ return;
+ }
+ // Is network connected?
+ // TODO(b/305259443): Use after-boot network connectivity projection
+ if (!isNetworkConnected(mContext)) {
+ Log.i(TAG, "Network is not connected, reschedule reboot.");
+ mInjector.triggerRebootOnNetworkAvailable(mContext);
+ return;
+ }
+ // Is current time between reboot window?
+ int currentHour =
+ Instant.ofEpochMilli(mInjector.now())
+ .atZone(mInjector.zoneId())
+ .toLocalDateTime()
+ .getHour();
+ if (currentHour < mInjector.getRebootStartTime()
+ || currentHour >= mInjector.getRebootEndTime()) {
+ Log.v(TAG, "Reboot requested outside of reboot window, reschedule reboot.");
+ prepareUnattendedReboot();
+ scheduleReboot();
+ return;
+ }
+ // Is preparing for SIM PIN replay successful?
+ if (enableSimPinReplay() && !mSimPinReplayManager.prepareSimPinReplay()) {
+ Log.w(TAG, "Sim Pin Replay failed, reschedule reboot");
+ scheduleReboot();
+ }
+
+ // Proceed with RoR.
+ Log.v(TAG, "Rebooting device to apply device config flags.");
+ try {
+ int success = mInjector.rebootAndApply(mContext, REBOOT_REASON, /* slotSwitch= */ false);
+ if (success != 0) {
+ // If reboot is not successful, reschedule.
+ Log.w(TAG, "Unattended reboot failed, reschedule reboot.");
+ scheduleReboot();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, e.getLocalizedMessage());
+ 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();
+ }
+
+ private static boolean isNetworkConnected(Context context) {
+ final ConnectivityManager connectivityManager =
+ context.getSystemService(ConnectivityManager.class);
+ if (connectivityManager == null) {
+ Log.w(TAG, "ConnectivityManager is null");
+ return false;
+ }
+ Network activeNetwork = connectivityManager.getActiveNetwork();
+ NetworkCapabilities networkCapabilities =
+ connectivityManager.getNetworkCapabilities(activeNetwork);
+ return networkCapabilities != null
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+ }
+}
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..5ca3e1e
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java
@@ -0,0 +1,50 @@
+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();
+
+ long elapsedRealtime();
+
+ /** Reboot time injectors. */
+ int getRebootStartTime();
+
+ int getRebootEndTime();
+
+ int getRebootFrequency();
+
+ /** Reboot Alarm injector. */
+ void setRebootAlarm(Context context, long rebootTimeMillis);
+
+ /** Connectivity injector. */
+ void triggerRebootOnNetworkAvailable(Context context);
+
+ /** {@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/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java
new file mode 100644
index 0000000..52ee5bd
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java
@@ -0,0 +1,150 @@
+/*
+ * 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.db;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.text.TextUtils;
+
+import com.android.server.deviceconfig.db.DeviceConfigDbHelper.Contract.DeviceConfigEntry;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @hide
+ */
+public class DeviceConfigDbAdapter {
+
+ private final SQLiteDatabase mDb;
+
+ public DeviceConfigDbAdapter(SQLiteDatabase db) {
+ mDb = db;
+ }
+
+ public Map<String, String> getValuesForNamespace(String namespace, String... keys) {
+
+ String[] projection = {
+ DeviceConfigEntry.COLUMN_NAME_KEY,
+ DeviceConfigEntry.COLUMN_NAME_VALUE
+ };
+
+ String selection;
+ String[] selectionArgs;
+ if (keys != null && keys.length > 0) {
+ selection = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ? "
+ + "and " + DeviceConfigEntry.COLUMN_NAME_KEY + " in ( ? ) ";
+ String keySelection = TextUtils.join(",", keys);
+ selectionArgs = new String[]{namespace, keySelection};
+ } else {
+ selection = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ?";
+ selectionArgs = new String[]{namespace};
+ }
+ Cursor cursor = mDb.query(
+ DeviceConfigEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArgs,
+ null,
+ null,
+ null
+ );
+
+ Map<String, String> map = new HashMap<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ String key = cursor.getString(
+ cursor.getColumnIndexOrThrow(DeviceConfigEntry.COLUMN_NAME_KEY));
+ String value = cursor.getString(
+ cursor.getColumnIndexOrThrow(DeviceConfigEntry.COLUMN_NAME_VALUE));
+ map.put(key, value);
+ }
+ cursor.close();
+ return map;
+ }
+
+ /**
+ *
+ * @return true if the data was inserted or updated in the database
+ */
+ private boolean insertOrUpdateValue_inTransaction(String namespace, String key, String value) {
+ // TODO(b/265948914): see if this is the most performant way to either insert or update a record
+ ContentValues values = new ContentValues();
+ values.put(DeviceConfigEntry.COLUMN_NAME_NAMESPACE, namespace);
+ values.put(DeviceConfigEntry.COLUMN_NAME_KEY, key);
+ values.put(DeviceConfigEntry.COLUMN_NAME_VALUE, value);
+
+ String where = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ? "
+ + "and " + DeviceConfigEntry.COLUMN_NAME_VALUE + " = ? ";
+
+ String[] whereArgs = {namespace, key};
+ int updatedRows = mDb.update(DeviceConfigEntry.TABLE_NAME, values, where, whereArgs);
+ if (updatedRows == 0) {
+ // this is a new row, we need to insert it
+ long id = mDb.insert(DeviceConfigEntry.TABLE_NAME, null, values);
+ return id != -1;
+ }
+ return updatedRows > 0;
+ }
+
+ /**
+ * Set or update the values in the map into the namespace.
+ *
+ * @return true if all values were set. Returns true if the map is empty.
+ */
+ public boolean setValues(String namespace, Map<String, String> map) {
+ if (map.size() == 0) {
+ return true;
+ }
+ boolean allSucceeded = true;
+ try {
+ mDb.beginTransaction();
+ for (Map.Entry<String, String> entry : map.entrySet()) {
+ // TODO(b/265948914) probably should call yieldIfContendedSafely in this loop
+ allSucceeded &= insertOrUpdateValue_inTransaction(namespace, entry.getKey(),
+ entry.getValue());
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ return allSucceeded;
+ }
+
+ /**
+ *
+ * @return true if the value was set
+ */
+ public boolean setValue(String namespace, String key, String value, boolean makeDefault) {
+ HashMap<String, String> map = new HashMap<>();
+ map.put(key, value);
+ return setValues(namespace, map);
+ // TODO(b/265948914) implement make default!
+ }
+
+ /**
+ *
+ * @return true if any value was deleted
+ */
+ public boolean deleteValue(String namespace, String key) {
+ String where = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ? "
+ + "and " + DeviceConfigEntry.COLUMN_NAME_KEY + " = ? ";
+ String[] whereArgs = { namespace, key };
+ int count = mDb.delete(DeviceConfigEntry.TABLE_NAME, where, whereArgs);
+ return count > 0;
+ }
+}
diff --git a/service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java
new file mode 100644
index 0000000..d7c90cc
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.db;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.BaseColumns;
+
+/**
+ * @hide
+ */
+public class DeviceConfigDbHelper extends SQLiteOpenHelper {
+ public static final int DATABASE_VERSION = 1;
+ public static final String DATABASE_NAME = "config_infrastructure.db";
+
+ /**
+ * TODO(b/265948914) / to consider:
+ *
+ * - enforce uniqueness of (namespace, key) pairs
+ * - synchronize calls that modify the db (maybe reads too?)
+ * - probably use a read/write lock
+ * - per-process caching of results so we don't go to the db every time
+ * - test the sql commands to make sure they work well (e.g. where clauses are
+ * written properly)
+ * - check the performance of the sql commands and look for optimizations
+ * - write a test for adapter.setProperties that has some but not all
+ * preexisting properties
+ * - Settings.Config has a concept "makeDefault" which is not implemented here
+ * - ensure that any sql exceptions are not thrown to the callers (where methods
+ * can return
+ * false)
+ * - see what happens if a caller starts observing changes before the database
+ * is loaded/ready (early in the boot process)
+ * - I've seen strict mode alerts about doing I/O in the main thread after a
+ * device boots. Maybe we can't avoid it but double check.
+ * - finish API implementation in DatabaseDataStore
+ */
+
+ interface Contract {
+ class DeviceConfigEntry implements BaseColumns {
+ public static final String TABLE_NAME = "config";
+ public static final String COLUMN_NAME_NAMESPACE = "namespace";
+ public static final String COLUMN_NAME_KEY = "config_key";
+ public static final String COLUMN_NAME_VALUE = "config_value";
+ }
+ }
+
+ private static final String SQL_CREATE_ENTRIES =
+ "CREATE TABLE " + Contract.DeviceConfigEntry.TABLE_NAME + " (" +
+ Contract.DeviceConfigEntry._ID + " INTEGER PRIMARY KEY," +
+ Contract.DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " TEXT," +
+ Contract.DeviceConfigEntry.COLUMN_NAME_KEY + " TEXT," +
+ Contract.DeviceConfigEntry.COLUMN_NAME_VALUE + " TEXT)";
+
+ public DeviceConfigDbHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_ENTRIES);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // no op for now
+ }
+
+}
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);
+ }
+ }
+ }
+}