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