blob: 4c4a0906070669c2a72faed542285d337236d034 [file] [log] [blame]
Hongyi Zhang50a4e012018-09-26 21:20:03 -07001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.am;
18
19import android.content.ContentResolver;
20import android.database.ContentObserver;
21import android.net.Uri;
22import android.os.Build;
23import android.os.SystemProperties;
24import android.provider.Settings;
25import android.text.TextUtils;
26import android.util.Slog;
27
28import com.android.internal.annotations.VisibleForTesting;
29
30import java.io.BufferedReader;
31import java.io.File;
32import java.io.FileReader;
33import java.io.IOException;
34import java.util.HashSet;
35
36/**
37 * Maps system settings to system properties.
38 * <p>The properties are dynamically updated when settings change.
39 */
40class SettingsToPropertiesMapper {
41
42 private static final String TAG = "SettingsToPropertiesMapper";
43
44 private static final String SYSTEM_PROPERTY_PREFIX = "persist.device_config.";
45
46 private static final String RESET_PERFORMED_PROPERTY = "device_config.reset_performed";
47
48 private static final String RESET_RECORD_FILE_PATH =
49 "/data/server_configurable_flags/reset_flags";
50
51 private static final String SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX = "^[\\w\\.\\-@:]*$";
52
53 private static final String SYSTEM_PROPERTY_INVALID_SUBSTRING = "..";
54
55 private static final int SYSTEM_PROPERTY_MAX_LENGTH = 92;
56
57 // experiment flags added to Global.Settings(before new "Config" provider table is available)
58 // will be added under this category.
59 private static final String GLOBAL_SETTINGS_CATEGORY = "global_settings";
60
61 // Add the global setting you want to push to native level as experiment flag into this list.
62 //
63 // NOTE: please grant write permission system property prefix
64 // with format persist.experiment.[experiment_category_name]. in system_server.te and grant read
65 // permission in the corresponding .te file your feature belongs to.
66 @VisibleForTesting
67 static final String[] sGlobalSettings = new String[] {
Hongyi Zhanga02118d2018-11-15 20:15:38 -080068 Settings.Global.NATIVE_FLAGS_HEALTH_CHECK_ENABLED,
Hongyi Zhang50a4e012018-09-26 21:20:03 -070069 };
70
71 @VisibleForTesting
72 static final String[] sDeviceConfigScopes = new String[] {
73 };
74
75 private final String[] mGlobalSettings;
76
77 private final String[] mDeviceConfigScopes;
78
79 private final ContentResolver mContentResolver;
80
81 @VisibleForTesting
82 protected SettingsToPropertiesMapper(ContentResolver contentResolver,
83 String[] globalSettings,
84 String[] deviceConfigScopes) {
85 mContentResolver = contentResolver;
86 mGlobalSettings = globalSettings;
87 mDeviceConfigScopes = deviceConfigScopes;
88 }
89
90 @VisibleForTesting
91 void updatePropertiesFromSettings() {
92 for (String globalSetting : mGlobalSettings) {
93 Uri settingUri = Settings.Global.getUriFor(globalSetting);
94 String propName = makePropertyName(GLOBAL_SETTINGS_CATEGORY, globalSetting);
95 if (settingUri == null) {
96 log("setting uri is null for globalSetting " + globalSetting);
97 continue;
98 }
99 if (propName == null) {
100 log("invalid prop name for globalSetting " + globalSetting);
101 continue;
102 }
103
104 ContentObserver co = new ContentObserver(null) {
105 @Override
106 public void onChange(boolean selfChange) {
107 updatePropertyFromSetting(globalSetting, propName, true);
108 }
109 };
110
111 // only updating on starting up when no native flags reset is performed during current
112 // booting.
113 if (!isNativeFlagsResetPerformed()) {
114 updatePropertyFromSetting(globalSetting, propName, true);
115 }
116 mContentResolver.registerContentObserver(settingUri, false, co);
117 }
118
119 // TODO: address sDeviceConfigScopes after DeviceConfig APIs are available.
120 }
121
122 public static SettingsToPropertiesMapper start(ContentResolver contentResolver) {
123 SettingsToPropertiesMapper mapper = new SettingsToPropertiesMapper(
124 contentResolver, sGlobalSettings, sDeviceConfigScopes);
125 mapper.updatePropertiesFromSettings();
126 return mapper;
127 }
128
129 /**
130 * If native level flags reset has been performed as an attempt to recover from a crash loop
131 * during current device booting.
132 * @return
133 */
134 public boolean isNativeFlagsResetPerformed() {
135 String value = systemPropertiesGet(RESET_PERFORMED_PROPERTY);
136 return "true".equals(value);
137 }
138
139 /**
140 * return an array of native flag categories under which flags got reset during current device
141 * booting.
142 * @return
143 */
144 public String[] getResetNativeCategories() {
145 if (!isNativeFlagsResetPerformed()) {
146 return new String[0];
147 }
148
149 String content = getResetFlagsFileContent();
150 if (TextUtils.isEmpty(content)) {
151 return new String[0];
152 }
153
154 String[] property_names = content.split(";");
155 HashSet<String> categories = new HashSet<>();
156 for (String property_name : property_names) {
157 String[] segments = property_name.split("\\.");
158 if (segments.length < 3) {
159 log("failed to extract category name from property " + property_name);
160 continue;
161 }
162 categories.add(segments[2]);
163 }
164 return categories.toArray(new String[0]);
165 }
166
167 /**
168 * system property name constructing rule: "persist.device_config.[category_name].[flag_name]".
169 * If the name contains invalid characters or substrings for system property name,
170 * will return null.
171 * @param categoryName
172 * @param flagName
173 * @return
174 */
175 @VisibleForTesting
176 static String makePropertyName(String categoryName, String flagName) {
177 String propertyName = SYSTEM_PROPERTY_PREFIX + categoryName + "." + flagName;
178
179 if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX)
180 || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) {
181 return null;
182 }
183
184 return propertyName;
185 }
186
187 private String getSetting(String name, boolean isGlobalSetting) {
188 if (isGlobalSetting) {
189 return Settings.Global.getString(mContentResolver, name);
190 } else {
191 // TODO: complete the code after DeviceConfig APIs implemented.
192 return null;
193 }
194 }
195
196 private void setProperty(String key, String value) {
197 // Check if need to clear the property
198 if (value == null) {
199 // It's impossible to remove system property, therefore we check previous value to
200 // avoid setting an empty string if the property wasn't set.
201 if (TextUtils.isEmpty(systemPropertiesGet(key))) {
202 return;
203 }
204 value = "";
205 } else if (value.length() > SYSTEM_PROPERTY_MAX_LENGTH) {
206 log(value + " exceeds system property max length.");
207 return;
208 }
209
210 try {
211 systemPropertiesSet(key, value);
212 } catch (Exception e) {
213 // Failure to set a property can be caused by SELinux denial. This usually indicates
214 // that the property wasn't whitelisted in sepolicy.
215 // No need to report it on all user devices, only on debug builds.
216 log("Unable to set property " + key + " value '" + value + "'", e);
217 }
218 }
219
220 private static void log(String msg, Exception e) {
221 if (Build.IS_DEBUGGABLE) {
222 Slog.wtf(TAG, msg, e);
223 } else {
224 Slog.e(TAG, msg, e);
225 }
226 }
227
228 private static void log(String msg) {
229 if (Build.IS_DEBUGGABLE) {
230 Slog.wtf(TAG, msg);
231 } else {
232 Slog.e(TAG, msg);
233 }
234 }
235
236 @VisibleForTesting
237 protected String systemPropertiesGet(String key) {
238 return SystemProperties.get(key);
239 }
240
241 @VisibleForTesting
242 protected void systemPropertiesSet(String key, String value) {
243 SystemProperties.set(key, value);
244 }
245
246 @VisibleForTesting
247 protected String getResetFlagsFileContent() {
248 String content = null;
249 try {
250 File reset_flag_file = new File(RESET_RECORD_FILE_PATH);
251 BufferedReader br = new BufferedReader(new FileReader(reset_flag_file));
252 content = br.readLine();
253
254 br.close();
255 } catch (IOException ioe) {
256 log("failed to read file " + RESET_RECORD_FILE_PATH, ioe);
257 }
258 return content;
259 }
260
261 @VisibleForTesting
262 void updatePropertyFromSetting(String settingName, String propName, boolean isGlobalSetting) {
263 String settingValue = getSetting(settingName, isGlobalSetting);
264 setProperty(propName, settingValue);
265 }
266}