GlobalSettingsToPropertiesMapper refactor
Only allow users to register category and flag names. Construct system
property names based on a consistent rule.
Bug: 111434018
Test: manual - registered a value and observed the change.
Change-Id: I9145daaf1793375ed8df7a3fb1878649cd4ced3e
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
new file mode 100644
index 0000000..a5848ca
--- /dev/null
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2018 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.am;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashSet;
+
+/**
+ * Maps system settings to system properties.
+ * <p>The properties are dynamically updated when settings change.
+ */
+class SettingsToPropertiesMapper {
+
+ private static final String TAG = "SettingsToPropertiesMapper";
+
+ private static final String SYSTEM_PROPERTY_PREFIX = "persist.device_config.";
+
+ private static final String RESET_PERFORMED_PROPERTY = "device_config.reset_performed";
+
+ private static final String RESET_RECORD_FILE_PATH =
+ "/data/server_configurable_flags/reset_flags";
+
+ private static final String SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX = "^[\\w\\.\\-@:]*$";
+
+ private static final String SYSTEM_PROPERTY_INVALID_SUBSTRING = "..";
+
+ private static final int SYSTEM_PROPERTY_MAX_LENGTH = 92;
+
+ // experiment flags added to Global.Settings(before new "Config" provider table is available)
+ // will be added under this category.
+ private static final String GLOBAL_SETTINGS_CATEGORY = "global_settings";
+
+ // Add the global setting you want to push to native level as experiment flag into this list.
+ //
+ // NOTE: please grant write permission system property prefix
+ // with format persist.experiment.[experiment_category_name]. in system_server.te and grant read
+ // permission in the corresponding .te file your feature belongs to.
+ @VisibleForTesting
+ static final String[] sGlobalSettings = new String[] {
+ };
+
+ @VisibleForTesting
+ static final String[] sDeviceConfigScopes = new String[] {
+ };
+
+ private final String[] mGlobalSettings;
+
+ private final String[] mDeviceConfigScopes;
+
+ private final ContentResolver mContentResolver;
+
+ @VisibleForTesting
+ protected SettingsToPropertiesMapper(ContentResolver contentResolver,
+ String[] globalSettings,
+ String[] deviceConfigScopes) {
+ mContentResolver = contentResolver;
+ mGlobalSettings = globalSettings;
+ mDeviceConfigScopes = deviceConfigScopes;
+ }
+
+ @VisibleForTesting
+ void updatePropertiesFromSettings() {
+ for (String globalSetting : mGlobalSettings) {
+ Uri settingUri = Settings.Global.getUriFor(globalSetting);
+ String propName = makePropertyName(GLOBAL_SETTINGS_CATEGORY, globalSetting);
+ if (settingUri == null) {
+ log("setting uri is null for globalSetting " + globalSetting);
+ continue;
+ }
+ if (propName == null) {
+ log("invalid prop name for globalSetting " + globalSetting);
+ continue;
+ }
+
+ ContentObserver co = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updatePropertyFromSetting(globalSetting, propName, true);
+ }
+ };
+
+ // only updating on starting up when no native flags reset is performed during current
+ // booting.
+ if (!isNativeFlagsResetPerformed()) {
+ updatePropertyFromSetting(globalSetting, propName, true);
+ }
+ mContentResolver.registerContentObserver(settingUri, false, co);
+ }
+
+ // TODO: address sDeviceConfigScopes after DeviceConfig APIs are available.
+ }
+
+ public static SettingsToPropertiesMapper start(ContentResolver contentResolver) {
+ SettingsToPropertiesMapper mapper = new SettingsToPropertiesMapper(
+ contentResolver, sGlobalSettings, sDeviceConfigScopes);
+ mapper.updatePropertiesFromSettings();
+ return mapper;
+ }
+
+ /**
+ * If native level flags reset has been performed as an attempt to recover from a crash loop
+ * during current device booting.
+ * @return
+ */
+ public boolean isNativeFlagsResetPerformed() {
+ String value = systemPropertiesGet(RESET_PERFORMED_PROPERTY);
+ return "true".equals(value);
+ }
+
+ /**
+ * return an array of native flag categories under which flags got reset during current device
+ * booting.
+ * @return
+ */
+ public String[] getResetNativeCategories() {
+ if (!isNativeFlagsResetPerformed()) {
+ return new String[0];
+ }
+
+ String content = getResetFlagsFileContent();
+ if (TextUtils.isEmpty(content)) {
+ return new String[0];
+ }
+
+ String[] property_names = content.split(";");
+ HashSet<String> categories = new HashSet<>();
+ for (String property_name : property_names) {
+ String[] segments = property_name.split("\\.");
+ if (segments.length < 3) {
+ log("failed to extract category name from property " + property_name);
+ continue;
+ }
+ categories.add(segments[2]);
+ }
+ return categories.toArray(new String[0]);
+ }
+
+ /**
+ * system property name constructing rule: "persist.device_config.[category_name].[flag_name]".
+ * If the name contains invalid characters or substrings for system property name,
+ * will return null.
+ * @param categoryName
+ * @param flagName
+ * @return
+ */
+ @VisibleForTesting
+ static String makePropertyName(String categoryName, String flagName) {
+ String propertyName = SYSTEM_PROPERTY_PREFIX + categoryName + "." + flagName;
+
+ if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX)
+ || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) {
+ return null;
+ }
+
+ return propertyName;
+ }
+
+ private String getSetting(String name, boolean isGlobalSetting) {
+ if (isGlobalSetting) {
+ return Settings.Global.getString(mContentResolver, name);
+ } else {
+ // TODO: complete the code after DeviceConfig APIs implemented.
+ return null;
+ }
+ }
+
+ private void setProperty(String key, String value) {
+ // Check if need to clear the property
+ if (value == null) {
+ // It's impossible to remove system property, therefore we check previous value to
+ // avoid setting an empty string if the property wasn't set.
+ if (TextUtils.isEmpty(systemPropertiesGet(key))) {
+ return;
+ }
+ value = "";
+ } else if (value.length() > SYSTEM_PROPERTY_MAX_LENGTH) {
+ log(value + " exceeds system property max length.");
+ return;
+ }
+
+ try {
+ systemPropertiesSet(key, value);
+ } catch (Exception e) {
+ // Failure to set a property can be caused by SELinux denial. This usually indicates
+ // that the property wasn't whitelisted in sepolicy.
+ // No need to report it on all user devices, only on debug builds.
+ log("Unable to set property " + key + " value '" + value + "'", e);
+ }
+ }
+
+ private static void log(String msg, Exception e) {
+ if (Build.IS_DEBUGGABLE) {
+ Slog.wtf(TAG, msg, e);
+ } else {
+ Slog.e(TAG, msg, e);
+ }
+ }
+
+ private static void log(String msg) {
+ if (Build.IS_DEBUGGABLE) {
+ Slog.wtf(TAG, msg);
+ } else {
+ Slog.e(TAG, msg);
+ }
+ }
+
+ @VisibleForTesting
+ protected String systemPropertiesGet(String key) {
+ return SystemProperties.get(key);
+ }
+
+ @VisibleForTesting
+ protected void systemPropertiesSet(String key, String value) {
+ SystemProperties.set(key, value);
+ }
+
+ @VisibleForTesting
+ protected String getResetFlagsFileContent() {
+ String content = null;
+ try {
+ File reset_flag_file = new File(RESET_RECORD_FILE_PATH);
+ BufferedReader br = new BufferedReader(new FileReader(reset_flag_file));
+ content = br.readLine();
+
+ br.close();
+ } catch (IOException ioe) {
+ log("failed to read file " + RESET_RECORD_FILE_PATH, ioe);
+ }
+ return content;
+ }
+
+ @VisibleForTesting
+ void updatePropertyFromSetting(String settingName, String propName, boolean isGlobalSetting) {
+ String settingValue = getSetting(settingName, isGlobalSetting);
+ setProperty(propName, settingValue);
+ }
+}