diff options
author | Xin Li <delphij@google.com> | 2024-03-06 09:30:08 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2024-03-06 09:30:08 -0800 |
commit | baba2caabc212bf23e261a02b58703830e23b1ff (patch) | |
tree | 4cd02b4c6548bfcd4db3aafa09468612f31cbf20 | |
parent | 4af146d89bb4ebcb721da15d55db2df8083e8ee3 (diff) | |
parent | 1f8c0a519e8b885f4a89f603bf8e5d24289b2e54 (diff) | |
download | ConfigInfrastructure-main.tar.gz |
Bug: 319669529
Merged-In: I6f4193a7a21630d874eb615c0c8ae5c295aa6227
Change-Id: Ia705359e7984526b97803dae6f7845088497007b
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); + } + } + } +} |