Loading services/core/java/com/android/server/am/ActivityManagerService.java +1 −1 Original line number Diff line number Diff line Loading @@ -7004,7 +7004,7 @@ public class ActivityManagerService extends IActivityManager.Stub mCoreSettingsObserver = new CoreSettingsObserver(this); mActivityTaskManager.installSystemProviders(); mDevelopmentSettingsObserver = new DevelopmentSettingsObserver(); GlobalSettingsToPropertiesMapper.start(mContext.getContentResolver()); SettingsToPropertiesMapper.start(mContext.getContentResolver()); // Now that the settings provider is published we can consider sending // in a rescue party. Loading services/core/java/com/android/server/am/GlobalSettingsToPropertiesMapper.javadeleted 100644 → 0 +0 −128 Original line number Diff line number Diff line /* * 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 android.view.ThreadedRenderer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; /** * Maps global system settings to system properties. * <p>The properties are dynamically updated when settings change. */ class GlobalSettingsToPropertiesMapper { private static final String TAG = "GlobalSettingsToPropertiesMapper"; // List mapping entries in the following format: // {Settings.Global.SETTING_NAME, "system_property_name"} // Important: Property being added should be whitelisted by SELinux policy or have one of the // already whitelisted prefixes in system_server.te, e.g. sys. private static final String[][] sGlobalSettingsMapping = new String[][] { {Settings.Global.SYS_VDSO, "sys.vdso"}, {Settings.Global.FPS_DEVISOR, ThreadedRenderer.DEBUG_FPS_DIVISOR}, {Settings.Global.DISPLAY_PANEL_LPM, "sys.display_panel_lpm"}, {Settings.Global.SYS_UIDCPUPOWER, "sys.uidcpupower"}, {Settings.Global.SYS_TRACED, "sys.traced.enable_override"}, }; private final ContentResolver mContentResolver; private final String[][] mGlobalSettingsMapping; @VisibleForTesting GlobalSettingsToPropertiesMapper(ContentResolver contentResolver, String[][] globalSettingsMapping) { mContentResolver = contentResolver; mGlobalSettingsMapping = globalSettingsMapping; } void updatePropertiesFromGlobalSettings() { for (String[] entry : mGlobalSettingsMapping) { final String settingName = entry[0]; final String propName = entry[1]; Uri settingUri = Settings.Global.getUriFor(settingName); Preconditions.checkNotNull(settingUri, "Setting " + settingName + " not found"); ContentObserver co = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { updatePropertyFromSetting(settingName, propName); } }; updatePropertyFromSetting(settingName, propName); mContentResolver.registerContentObserver(settingUri, false, co); } } public static void start(ContentResolver contentResolver) { new GlobalSettingsToPropertiesMapper(contentResolver, sGlobalSettingsMapping) .updatePropertiesFromGlobalSettings(); } private String getGlobalSetting(String name) { return Settings.Global.getString(mContentResolver, name); } 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 = ""; } 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. if (Build.IS_DEBUGGABLE) { Slog.wtf(TAG, "Unable to set property " + key + " value '" + value + "'", e); } else { Slog.e(TAG, "Unable to set property " + key + " value '" + value + "'", e); } } } @VisibleForTesting protected String systemPropertiesGet(String key) { return SystemProperties.get(key); } @VisibleForTesting protected void systemPropertiesSet(String key, String value) { SystemProperties.set(key, value); } @VisibleForTesting void updatePropertyFromSetting(String settingName, String propName) { String settingValue = getGlobalSetting(settingName); setProperty(propName, settingValue); } } services/core/java/com/android/server/am/OWNERS +1 −1 Original line number Diff line number Diff line Loading @@ -27,4 +27,4 @@ toddke@google.com michaelwr@google.com narayan@google.com per-file GlobalSettingsToPropertiesMapper.java = omakoto@google.com, svetoslavganov@google.com, yamasani@google.com per-file SettingsToPropertiesMapper.java = omakoto@google.com, svetoslavganov@google.com, yamasani@google.com services/core/java/com/android/server/am/SettingsToPropertiesMapper.java 0 → 100644 +265 −0 Original line number Diff line number Diff line /* * 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); } } services/tests/servicestests/src/com/android/server/am/GlobalSettingsToPropertiesMapperTest.java→services/tests/servicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java +211 −0 Original line number Diff line number Diff line Loading @@ -16,86 +16,174 @@ package com.android.server.am; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import android.content.ContentResolver; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.text.TextUtils; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.Preconditions; import com.android.internal.util.test.FakeSettingsProvider; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; /** * Tests for {@link GlobalSettingsToPropertiesMapper} * * Build/Install/Run: * atest FrameworksServicesTests:GlobalSettingsToPropertiesMapperTest * Tests for {@link SettingsToPropertiesMapper} */ @RunWith(AndroidJUnit4.class) @SmallTest public class GlobalSettingsToPropertiesMapperTest { private static final String[][] TEST_MAPPING = new String[][] { {Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"} public class SettingsToPropertiesMapperTest { private static final String NAME_VALID_CHARACTERS_REGEX = "^[\\w\\.\\-@:]*$"; private static final String[] TEST_MAPPING = new String[] { Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS }; private TestMapper mTestMapper; private MockContentResolver mMockContentResolver; @Before public void setup() { public void setupForEach() { // Use FakeSettingsProvider to not affect global state mMockContentResolver = new MockContentResolver(getInstrumentation().getTargetContext()); mMockContentResolver = new MockContentResolver(InstrumentationRegistry.getContext()); mMockContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); mTestMapper = new TestMapper(mMockContentResolver); } @Test public void testUpdatePropertiesFromGlobalSettings() { public void validateRegisteredGlobalSettings() { HashSet<String> hashSet = new HashSet<>(); for (String globalSetting : SettingsToPropertiesMapper.sGlobalSettings) { if (hashSet.contains(globalSetting)) { Assert.fail("globalSetting " + globalSetting + " is registered more than once in " + "SettingsToPropertiesMapper.sGlobalSettings."); } hashSet.add(globalSetting); if (TextUtils.isEmpty(globalSetting)) { Assert.fail("empty globalSetting registered."); } if (!globalSetting.matches(NAME_VALID_CHARACTERS_REGEX)) { Assert.fail(globalSetting + " contains invalid characters. " + "Only alphanumeric characters, '.', '-', '@', ':' and '_' are valid."); } } } @Test public void testUpdatePropertiesFromSettings() { Settings.Global.putString(mMockContentResolver, Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "testValue"); mTestMapper.updatePropertiesFromGlobalSettings(); String propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertEquals("testValue", propValue); String systemPropertyName = "persist.device_config.global_settings." + "sqlite_compatibility_wal_flags"; mTestMapper.updatePropertiesFromSettings(); String propValue = mTestMapper.systemPropertiesGet(systemPropertyName); Assert.assertEquals("testValue", propValue); Settings.Global.putString(mMockContentResolver, Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "testValue2"); mTestMapper.updatePropertyFromSetting(Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"); propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertEquals("testValue2", propValue); mTestMapper.updatePropertyFromSetting( Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, systemPropertyName, true); propValue = mTestMapper.systemPropertiesGet(systemPropertyName); Assert.assertEquals("testValue2", propValue); Settings.Global.putString(mMockContentResolver, Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, null); mTestMapper.updatePropertyFromSetting(Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"); propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertEquals("", propValue); mTestMapper.updatePropertyFromSetting( Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, systemPropertyName, true); propValue = mTestMapper.systemPropertiesGet(systemPropertyName); Assert.assertEquals("", propValue); } @Test public void testUpdatePropertiesFromGlobalSettings_PropertyAndSettingNotPresent() { public void testMakePropertyName() { try { Assert.assertEquals("persist.device_config.test_category.test_flag", SettingsToPropertiesMapper.makePropertyName("test_category", "test_flag")); } catch (Exception e) { Assert.fail("Unexpected exception: " + e.getMessage()); } try { Assert.assertEquals(null, SettingsToPropertiesMapper.makePropertyName("test_category!!!", "test_flag")); } catch (Exception e) { Assert.fail("Unexpected exception: " + e.getMessage()); } try { Assert.assertEquals(null, SettingsToPropertiesMapper.makePropertyName("test_category", ".test_flag")); } catch (Exception e) { Assert.fail("Unexpected exception: " + e.getMessage()); } } @Test public void testUpdatePropertiesFromSettings_PropertyAndSettingNotPresent() { // Test that empty property will not not be set if setting is not set mTestMapper.updatePropertiesFromGlobalSettings(); mTestMapper.updatePropertiesFromSettings(); String propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertNull("Property should not be set if setting is null", propValue); Assert.assertNull("Property should not be set if setting is null", propValue); } private static class TestMapper extends GlobalSettingsToPropertiesMapper { @Test public void testIsNativeFlagsResetPerformed() { mTestMapper.systemPropertiesSet("device_config.reset_performed", "true"); Assert.assertTrue(mTestMapper.isNativeFlagsResetPerformed()); mTestMapper.systemPropertiesSet("device_config.reset_performed", "false"); Assert.assertFalse(mTestMapper.isNativeFlagsResetPerformed()); mTestMapper.systemPropertiesSet("device_config.reset_performed", ""); Assert.assertFalse(mTestMapper.isNativeFlagsResetPerformed()); } @Test public void testGetResetNativeCategories() { mTestMapper.systemPropertiesSet("device_config.reset_performed", ""); Assert.assertEquals(mTestMapper.getResetNativeCategories().length, 0); mTestMapper.systemPropertiesSet("device_config.reset_performed", "true"); mTestMapper.setFileContent(""); Assert.assertEquals(mTestMapper.getResetNativeCategories().length, 0); mTestMapper.systemPropertiesSet("device_config.reset_performed", "true"); mTestMapper.setFileContent("persist.device_config.category1.flag;" + "persist.device_config.category2.flag;persist.device_config.category3.flag;" + "persist.device_config.category3.flag2"); List<String> categories = Arrays.asList(mTestMapper.getResetNativeCategories()); Assert.assertEquals(3, categories.size()); Assert.assertTrue(categories.contains("category1")); Assert.assertTrue(categories.contains("category2")); Assert.assertTrue(categories.contains("category3")); } private static class TestMapper extends SettingsToPropertiesMapper { private final Map<String, String> mProps = new HashMap<>(); private String mFileContent = ""; TestMapper(ContentResolver contentResolver) { super(contentResolver, TEST_MAPPING); super(contentResolver, TEST_MAPPING, new String[] {}); } @Override Loading @@ -110,6 +198,14 @@ public class GlobalSettingsToPropertiesMapperTest { Preconditions.checkNotNull(key); mProps.put(key, value); } } protected void setFileContent(String fileContent) { mFileContent = fileContent; } @Override protected String getResetFlagsFileContent() { return mFileContent; } } } Loading
services/core/java/com/android/server/am/ActivityManagerService.java +1 −1 Original line number Diff line number Diff line Loading @@ -7004,7 +7004,7 @@ public class ActivityManagerService extends IActivityManager.Stub mCoreSettingsObserver = new CoreSettingsObserver(this); mActivityTaskManager.installSystemProviders(); mDevelopmentSettingsObserver = new DevelopmentSettingsObserver(); GlobalSettingsToPropertiesMapper.start(mContext.getContentResolver()); SettingsToPropertiesMapper.start(mContext.getContentResolver()); // Now that the settings provider is published we can consider sending // in a rescue party. Loading
services/core/java/com/android/server/am/GlobalSettingsToPropertiesMapper.javadeleted 100644 → 0 +0 −128 Original line number Diff line number Diff line /* * 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 android.view.ThreadedRenderer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; /** * Maps global system settings to system properties. * <p>The properties are dynamically updated when settings change. */ class GlobalSettingsToPropertiesMapper { private static final String TAG = "GlobalSettingsToPropertiesMapper"; // List mapping entries in the following format: // {Settings.Global.SETTING_NAME, "system_property_name"} // Important: Property being added should be whitelisted by SELinux policy or have one of the // already whitelisted prefixes in system_server.te, e.g. sys. private static final String[][] sGlobalSettingsMapping = new String[][] { {Settings.Global.SYS_VDSO, "sys.vdso"}, {Settings.Global.FPS_DEVISOR, ThreadedRenderer.DEBUG_FPS_DIVISOR}, {Settings.Global.DISPLAY_PANEL_LPM, "sys.display_panel_lpm"}, {Settings.Global.SYS_UIDCPUPOWER, "sys.uidcpupower"}, {Settings.Global.SYS_TRACED, "sys.traced.enable_override"}, }; private final ContentResolver mContentResolver; private final String[][] mGlobalSettingsMapping; @VisibleForTesting GlobalSettingsToPropertiesMapper(ContentResolver contentResolver, String[][] globalSettingsMapping) { mContentResolver = contentResolver; mGlobalSettingsMapping = globalSettingsMapping; } void updatePropertiesFromGlobalSettings() { for (String[] entry : mGlobalSettingsMapping) { final String settingName = entry[0]; final String propName = entry[1]; Uri settingUri = Settings.Global.getUriFor(settingName); Preconditions.checkNotNull(settingUri, "Setting " + settingName + " not found"); ContentObserver co = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { updatePropertyFromSetting(settingName, propName); } }; updatePropertyFromSetting(settingName, propName); mContentResolver.registerContentObserver(settingUri, false, co); } } public static void start(ContentResolver contentResolver) { new GlobalSettingsToPropertiesMapper(contentResolver, sGlobalSettingsMapping) .updatePropertiesFromGlobalSettings(); } private String getGlobalSetting(String name) { return Settings.Global.getString(mContentResolver, name); } 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 = ""; } 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. if (Build.IS_DEBUGGABLE) { Slog.wtf(TAG, "Unable to set property " + key + " value '" + value + "'", e); } else { Slog.e(TAG, "Unable to set property " + key + " value '" + value + "'", e); } } } @VisibleForTesting protected String systemPropertiesGet(String key) { return SystemProperties.get(key); } @VisibleForTesting protected void systemPropertiesSet(String key, String value) { SystemProperties.set(key, value); } @VisibleForTesting void updatePropertyFromSetting(String settingName, String propName) { String settingValue = getGlobalSetting(settingName); setProperty(propName, settingValue); } }
services/core/java/com/android/server/am/OWNERS +1 −1 Original line number Diff line number Diff line Loading @@ -27,4 +27,4 @@ toddke@google.com michaelwr@google.com narayan@google.com per-file GlobalSettingsToPropertiesMapper.java = omakoto@google.com, svetoslavganov@google.com, yamasani@google.com per-file SettingsToPropertiesMapper.java = omakoto@google.com, svetoslavganov@google.com, yamasani@google.com
services/core/java/com/android/server/am/SettingsToPropertiesMapper.java 0 → 100644 +265 −0 Original line number Diff line number Diff line /* * 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); } }
services/tests/servicestests/src/com/android/server/am/GlobalSettingsToPropertiesMapperTest.java→services/tests/servicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java +211 −0 Original line number Diff line number Diff line Loading @@ -16,86 +16,174 @@ package com.android.server.am; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import android.content.ContentResolver; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.text.TextUtils; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.Preconditions; import com.android.internal.util.test.FakeSettingsProvider; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; /** * Tests for {@link GlobalSettingsToPropertiesMapper} * * Build/Install/Run: * atest FrameworksServicesTests:GlobalSettingsToPropertiesMapperTest * Tests for {@link SettingsToPropertiesMapper} */ @RunWith(AndroidJUnit4.class) @SmallTest public class GlobalSettingsToPropertiesMapperTest { private static final String[][] TEST_MAPPING = new String[][] { {Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"} public class SettingsToPropertiesMapperTest { private static final String NAME_VALID_CHARACTERS_REGEX = "^[\\w\\.\\-@:]*$"; private static final String[] TEST_MAPPING = new String[] { Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS }; private TestMapper mTestMapper; private MockContentResolver mMockContentResolver; @Before public void setup() { public void setupForEach() { // Use FakeSettingsProvider to not affect global state mMockContentResolver = new MockContentResolver(getInstrumentation().getTargetContext()); mMockContentResolver = new MockContentResolver(InstrumentationRegistry.getContext()); mMockContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); mTestMapper = new TestMapper(mMockContentResolver); } @Test public void testUpdatePropertiesFromGlobalSettings() { public void validateRegisteredGlobalSettings() { HashSet<String> hashSet = new HashSet<>(); for (String globalSetting : SettingsToPropertiesMapper.sGlobalSettings) { if (hashSet.contains(globalSetting)) { Assert.fail("globalSetting " + globalSetting + " is registered more than once in " + "SettingsToPropertiesMapper.sGlobalSettings."); } hashSet.add(globalSetting); if (TextUtils.isEmpty(globalSetting)) { Assert.fail("empty globalSetting registered."); } if (!globalSetting.matches(NAME_VALID_CHARACTERS_REGEX)) { Assert.fail(globalSetting + " contains invalid characters. " + "Only alphanumeric characters, '.', '-', '@', ':' and '_' are valid."); } } } @Test public void testUpdatePropertiesFromSettings() { Settings.Global.putString(mMockContentResolver, Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "testValue"); mTestMapper.updatePropertiesFromGlobalSettings(); String propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertEquals("testValue", propValue); String systemPropertyName = "persist.device_config.global_settings." + "sqlite_compatibility_wal_flags"; mTestMapper.updatePropertiesFromSettings(); String propValue = mTestMapper.systemPropertiesGet(systemPropertyName); Assert.assertEquals("testValue", propValue); Settings.Global.putString(mMockContentResolver, Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "testValue2"); mTestMapper.updatePropertyFromSetting(Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"); propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertEquals("testValue2", propValue); mTestMapper.updatePropertyFromSetting( Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, systemPropertyName, true); propValue = mTestMapper.systemPropertiesGet(systemPropertyName); Assert.assertEquals("testValue2", propValue); Settings.Global.putString(mMockContentResolver, Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, null); mTestMapper.updatePropertyFromSetting(Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, "TestProperty"); propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertEquals("", propValue); mTestMapper.updatePropertyFromSetting( Settings.Global.SQLITE_COMPATIBILITY_WAL_FLAGS, systemPropertyName, true); propValue = mTestMapper.systemPropertiesGet(systemPropertyName); Assert.assertEquals("", propValue); } @Test public void testUpdatePropertiesFromGlobalSettings_PropertyAndSettingNotPresent() { public void testMakePropertyName() { try { Assert.assertEquals("persist.device_config.test_category.test_flag", SettingsToPropertiesMapper.makePropertyName("test_category", "test_flag")); } catch (Exception e) { Assert.fail("Unexpected exception: " + e.getMessage()); } try { Assert.assertEquals(null, SettingsToPropertiesMapper.makePropertyName("test_category!!!", "test_flag")); } catch (Exception e) { Assert.fail("Unexpected exception: " + e.getMessage()); } try { Assert.assertEquals(null, SettingsToPropertiesMapper.makePropertyName("test_category", ".test_flag")); } catch (Exception e) { Assert.fail("Unexpected exception: " + e.getMessage()); } } @Test public void testUpdatePropertiesFromSettings_PropertyAndSettingNotPresent() { // Test that empty property will not not be set if setting is not set mTestMapper.updatePropertiesFromGlobalSettings(); mTestMapper.updatePropertiesFromSettings(); String propValue = mTestMapper.systemPropertiesGet("TestProperty"); assertNull("Property should not be set if setting is null", propValue); Assert.assertNull("Property should not be set if setting is null", propValue); } private static class TestMapper extends GlobalSettingsToPropertiesMapper { @Test public void testIsNativeFlagsResetPerformed() { mTestMapper.systemPropertiesSet("device_config.reset_performed", "true"); Assert.assertTrue(mTestMapper.isNativeFlagsResetPerformed()); mTestMapper.systemPropertiesSet("device_config.reset_performed", "false"); Assert.assertFalse(mTestMapper.isNativeFlagsResetPerformed()); mTestMapper.systemPropertiesSet("device_config.reset_performed", ""); Assert.assertFalse(mTestMapper.isNativeFlagsResetPerformed()); } @Test public void testGetResetNativeCategories() { mTestMapper.systemPropertiesSet("device_config.reset_performed", ""); Assert.assertEquals(mTestMapper.getResetNativeCategories().length, 0); mTestMapper.systemPropertiesSet("device_config.reset_performed", "true"); mTestMapper.setFileContent(""); Assert.assertEquals(mTestMapper.getResetNativeCategories().length, 0); mTestMapper.systemPropertiesSet("device_config.reset_performed", "true"); mTestMapper.setFileContent("persist.device_config.category1.flag;" + "persist.device_config.category2.flag;persist.device_config.category3.flag;" + "persist.device_config.category3.flag2"); List<String> categories = Arrays.asList(mTestMapper.getResetNativeCategories()); Assert.assertEquals(3, categories.size()); Assert.assertTrue(categories.contains("category1")); Assert.assertTrue(categories.contains("category2")); Assert.assertTrue(categories.contains("category3")); } private static class TestMapper extends SettingsToPropertiesMapper { private final Map<String, String> mProps = new HashMap<>(); private String mFileContent = ""; TestMapper(ContentResolver contentResolver) { super(contentResolver, TEST_MAPPING); super(contentResolver, TEST_MAPPING, new String[] {}); } @Override Loading @@ -110,6 +198,14 @@ public class GlobalSettingsToPropertiesMapperTest { Preconditions.checkNotNull(key); mProps.put(key, value); } } protected void setFileContent(String fileContent) { mFileContent = fileContent; } @Override protected String getResetFlagsFileContent() { return mFileContent; } } }