Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit dc21d50b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "GlobalSettingsToPropertiesMapper refactor"

parents 102ecc03 50a4e018
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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.
+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);
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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
+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);
    }
}
+211 −0
Original line number Diff line number Diff line
@@ -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
@@ -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