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