blob: a5848ca0235aa4c652f6e14475ca11de6f0a21be [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[] {
68 };
69
70 @VisibleForTesting
71 static final String[] sDeviceConfigScopes = new String[] {
72 };
73
74 private final String[] mGlobalSettings;
75
76 private final String[] mDeviceConfigScopes;
77
78 private final ContentResolver mContentResolver;
79
80 @VisibleForTesting
81 protected SettingsToPropertiesMapper(ContentResolver contentResolver,
82 String[] globalSettings,
83 String[] deviceConfigScopes) {
84 mContentResolver = contentResolver;
85 mGlobalSettings = globalSettings;
86 mDeviceConfigScopes = deviceConfigScopes;
87 }
88
89 @VisibleForTesting
90 void updatePropertiesFromSettings() {
91 for (String globalSetting : mGlobalSettings) {
92 Uri settingUri = Settings.Global.getUriFor(globalSetting);
93 String propName = makePropertyName(GLOBAL_SETTINGS_CATEGORY, globalSetting);
94 if (settingUri == null) {
95 log("setting uri is null for globalSetting " + globalSetting);
96 continue;
97 }
98 if (propName == null) {
99 log("invalid prop name for globalSetting " + globalSetting);
100 continue;
101 }
102
103 ContentObserver co = new ContentObserver(null) {
104 @Override
105 public void onChange(boolean selfChange) {
106 updatePropertyFromSetting(globalSetting, propName, true);
107 }
108 };
109
110 // only updating on starting up when no native flags reset is performed during current
111 // booting.
112 if (!isNativeFlagsResetPerformed()) {
113 updatePropertyFromSetting(globalSetting, propName, true);
114 }
115 mContentResolver.registerContentObserver(settingUri, false, co);
116 }
117
118 // TODO: address sDeviceConfigScopes after DeviceConfig APIs are available.
119 }
120
121 public static SettingsToPropertiesMapper start(ContentResolver contentResolver) {
122 SettingsToPropertiesMapper mapper = new SettingsToPropertiesMapper(
123 contentResolver, sGlobalSettings, sDeviceConfigScopes);
124 mapper.updatePropertiesFromSettings();
125 return mapper;
126 }
127
128 /**
129 * If native level flags reset has been performed as an attempt to recover from a crash loop
130 * during current device booting.
131 * @return
132 */
133 public boolean isNativeFlagsResetPerformed() {
134 String value = systemPropertiesGet(RESET_PERFORMED_PROPERTY);
135 return "true".equals(value);
136 }
137
138 /**
139 * return an array of native flag categories under which flags got reset during current device
140 * booting.
141 * @return
142 */
143 public String[] getResetNativeCategories() {
144 if (!isNativeFlagsResetPerformed()) {
145 return new String[0];
146 }
147
148 String content = getResetFlagsFileContent();
149 if (TextUtils.isEmpty(content)) {
150 return new String[0];
151 }
152
153 String[] property_names = content.split(";");
154 HashSet<String> categories = new HashSet<>();
155 for (String property_name : property_names) {
156 String[] segments = property_name.split("\\.");
157 if (segments.length < 3) {
158 log("failed to extract category name from property " + property_name);
159 continue;
160 }
161 categories.add(segments[2]);
162 }
163 return categories.toArray(new String[0]);
164 }
165
166 /**
167 * system property name constructing rule: "persist.device_config.[category_name].[flag_name]".
168 * If the name contains invalid characters or substrings for system property name,
169 * will return null.
170 * @param categoryName
171 * @param flagName
172 * @return
173 */
174 @VisibleForTesting
175 static String makePropertyName(String categoryName, String flagName) {
176 String propertyName = SYSTEM_PROPERTY_PREFIX + categoryName + "." + flagName;
177
178 if (!propertyName.matches(SYSTEM_PROPERTY_VALID_CHARACTERS_REGEX)
179 || propertyName.contains(SYSTEM_PROPERTY_INVALID_SUBSTRING)) {
180 return null;
181 }
182
183 return propertyName;
184 }
185
186 private String getSetting(String name, boolean isGlobalSetting) {
187 if (isGlobalSetting) {
188 return Settings.Global.getString(mContentResolver, name);
189 } else {
190 // TODO: complete the code after DeviceConfig APIs implemented.
191 return null;
192 }
193 }
194
195 private void setProperty(String key, String value) {
196 // Check if need to clear the property
197 if (value == null) {
198 // It's impossible to remove system property, therefore we check previous value to
199 // avoid setting an empty string if the property wasn't set.
200 if (TextUtils.isEmpty(systemPropertiesGet(key))) {
201 return;
202 }
203 value = "";
204 } else if (value.length() > SYSTEM_PROPERTY_MAX_LENGTH) {
205 log(value + " exceeds system property max length.");
206 return;
207 }
208
209 try {
210 systemPropertiesSet(key, value);
211 } catch (Exception e) {
212 // Failure to set a property can be caused by SELinux denial. This usually indicates
213 // that the property wasn't whitelisted in sepolicy.
214 // No need to report it on all user devices, only on debug builds.
215 log("Unable to set property " + key + " value '" + value + "'", e);
216 }
217 }
218
219 private static void log(String msg, Exception e) {
220 if (Build.IS_DEBUGGABLE) {
221 Slog.wtf(TAG, msg, e);
222 } else {
223 Slog.e(TAG, msg, e);
224 }
225 }
226
227 private static void log(String msg) {
228 if (Build.IS_DEBUGGABLE) {
229 Slog.wtf(TAG, msg);
230 } else {
231 Slog.e(TAG, msg);
232 }
233 }
234
235 @VisibleForTesting
236 protected String systemPropertiesGet(String key) {
237 return SystemProperties.get(key);
238 }
239
240 @VisibleForTesting
241 protected void systemPropertiesSet(String key, String value) {
242 SystemProperties.set(key, value);
243 }
244
245 @VisibleForTesting
246 protected String getResetFlagsFileContent() {
247 String content = null;
248 try {
249 File reset_flag_file = new File(RESET_RECORD_FILE_PATH);
250 BufferedReader br = new BufferedReader(new FileReader(reset_flag_file));
251 content = br.readLine();
252
253 br.close();
254 } catch (IOException ioe) {
255 log("failed to read file " + RESET_RECORD_FILE_PATH, ioe);
256 }
257 return content;
258 }
259
260 @VisibleForTesting
261 void updatePropertyFromSetting(String settingName, String propName, boolean isGlobalSetting) {
262 String settingValue = getSetting(settingName, isGlobalSetting);
263 setProperty(propName, settingValue);
264 }
265}