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