1 /* 2 * Copyright (C) 2024 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.internal.pm.pkg.component; 18 19 import static com.android.internal.pm.pkg.parsing.ParsingUtils.ANDROID_RES_NAMESPACE; 20 21 import android.aconfig.nano.Aconfig; 22 import android.aconfig.nano.Aconfig.parsed_flag; 23 import android.aconfig.nano.Aconfig.parsed_flags; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.res.Flags; 27 import android.content.res.XmlResourceParser; 28 import android.os.Environment; 29 import android.os.Process; 30 import android.util.ArrayMap; 31 import android.util.Slog; 32 import android.util.Xml; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.modules.utils.TypedXmlPullParser; 36 37 import org.xmlpull.v1.XmlPullParser; 38 import org.xmlpull.v1.XmlPullParserException; 39 40 import java.io.File; 41 import java.io.FileInputStream; 42 import java.io.IOException; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * A class that manages a cache of all device feature flags and their default + override values. 48 * This class performs a very similar job to the one in {@code SettingsProvider}, with an important 49 * difference: this is a part of system server and is available for the server startup. Package 50 * parsing happens at the startup when {@code SettingsProvider} isn't available yet, so we need an 51 * own copy of the code here. 52 * @hide 53 */ 54 public class AconfigFlags { 55 private static final String LOG_TAG = "AconfigFlags"; 56 57 private static final List<String> sTextProtoFilesOnDevice = List.of( 58 "/system/etc/aconfig_flags.pb", 59 "/system_ext/etc/aconfig_flags.pb", 60 "/product/etc/aconfig_flags.pb", 61 "/vendor/etc/aconfig_flags.pb"); 62 63 private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>(); 64 AconfigFlags()65 public AconfigFlags() { 66 if (!Flags.manifestFlagging()) { 67 Slog.v(LOG_TAG, "Feature disabled, skipped all loading"); 68 return; 69 } 70 for (String fileName : sTextProtoFilesOnDevice) { 71 try (var inputStream = new FileInputStream(fileName)) { 72 loadAconfigDefaultValues(inputStream.readAllBytes()); 73 } catch (IOException e) { 74 Slog.e(LOG_TAG, "Failed to read Aconfig values from " + fileName, e); 75 } 76 } 77 if (Process.myUid() == Process.SYSTEM_UID) { 78 // Server overrides are only accessible to the system, no need to even try loading them 79 // in user processes. 80 loadServerOverrides(); 81 } 82 } 83 loadServerOverrides()84 private void loadServerOverrides() { 85 // Reading the proto files is enough for READ_ONLY flags but if it's a READ_WRITE flag 86 // (which you can check with `flag.getPermission() == flag_permission.READ_WRITE`) then we 87 // also need to check if there is a value pushed from the server in the file 88 // `/data/system/users/0/settings_config.xml`. It will be in a <setting> node under the 89 // root <settings> node with "name" attribute == "flag_namespace/flag_package.flag_name". 90 // The "value" attribute will be true or false. 91 // 92 // The "name" attribute could also be "<namespace>/flag_namespace?flag_package.flag_name" 93 // (prefixed with "staged/" or "device_config_overrides/" and a different separator between 94 // namespace and name). This happens when a flag value is overridden either with a pushed 95 // one from the server, or from the local command. 96 // When the device reboots during package parsing, the staged value will still be there and 97 // only later it will become a regular/non-staged value after SettingsProvider is 98 // initialized. 99 // 100 // In all cases, when there is more than one value, the priority is: 101 // device_config_overrides > staged > default 102 // 103 104 final var settingsFile = new File(Environment.getUserSystemDirectory(0), 105 "settings_config.xml"); 106 try (var inputStream = new FileInputStream(settingsFile)) { 107 TypedXmlPullParser parser = Xml.resolvePullParser(inputStream); 108 if (parser.next() != XmlPullParser.END_TAG && "settings".equals(parser.getName())) { 109 final var flagPriority = new ArrayMap<String, Integer>(); 110 final int outerDepth = parser.getDepth(); 111 int type; 112 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 113 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 114 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 115 continue; 116 } 117 if (!"setting".equals(parser.getName())) { 118 continue; 119 } 120 String name = parser.getAttributeValue(null, "name"); 121 final String value = parser.getAttributeValue(null, "value"); 122 if (name == null || value == null) { 123 continue; 124 } 125 // A non-boolean setting is definitely not an Aconfig flag value. 126 if (!"false".equalsIgnoreCase(value) && !"true".equalsIgnoreCase(value)) { 127 continue; 128 } 129 final var overridePrefix = "device_config_overrides/"; 130 final var stagedPrefix = "staged/"; 131 String separator = "/"; 132 String prefix = "default"; 133 int priority = 0; 134 if (name.startsWith(overridePrefix)) { 135 prefix = overridePrefix; 136 name = name.substring(overridePrefix.length()); 137 separator = ":"; 138 priority = 20; 139 } else if (name.startsWith(stagedPrefix)) { 140 prefix = stagedPrefix; 141 name = name.substring(stagedPrefix.length()); 142 separator = "*"; 143 priority = 10; 144 } 145 final String flagPackageAndName = parseFlagPackageAndName(name, separator); 146 if (flagPackageAndName == null) { 147 continue; 148 } 149 // We ignore all settings that aren't for flags. We'll know they are for flags 150 // if they correspond to flags read from the proto files. 151 if (!mFlagValues.containsKey(flagPackageAndName)) { 152 continue; 153 } 154 Slog.d(LOG_TAG, "Found " + prefix 155 + " Aconfig flag value for " + flagPackageAndName + " = " + value); 156 final Integer currentPriority = flagPriority.get(flagPackageAndName); 157 if (currentPriority != null && currentPriority >= priority) { 158 Slog.i(LOG_TAG, "Skipping " + prefix + " flag " + flagPackageAndName 159 + " because of the existing one with priority " + currentPriority); 160 continue; 161 } 162 flagPriority.put(flagPackageAndName, priority); 163 mFlagValues.put(flagPackageAndName, Boolean.parseBoolean(value)); 164 } 165 } 166 } catch (IOException | XmlPullParserException e) { 167 Slog.e(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e); 168 } 169 } 170 parseFlagPackageAndName(String fullName, String separator)171 private static String parseFlagPackageAndName(String fullName, String separator) { 172 int index = fullName.indexOf(separator); 173 if (index < 0) { 174 return null; 175 } 176 return fullName.substring(index + 1); 177 } 178 loadAconfigDefaultValues(byte[] fileContents)179 private void loadAconfigDefaultValues(byte[] fileContents) throws IOException { 180 parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); 181 for (parsed_flag flag : parsedFlags.parsedFlag) { 182 String flagPackageAndName = flag.package_ + "." + flag.name; 183 boolean flagValue = (flag.state == Aconfig.ENABLED); 184 Slog.v(LOG_TAG, "Read Aconfig default flag value " 185 + flagPackageAndName + " = " + flagValue); 186 mFlagValues.put(flagPackageAndName, flagValue); 187 } 188 } 189 190 /** 191 * Get the flag value, or null if the flag doesn't exist. 192 * @param flagPackageAndName Full flag name formatted as 'package.flag' 193 * @return the current value of the given Aconfig flag, or null if there is no such flag 194 */ 195 @Nullable getFlagValue(@onNull String flagPackageAndName)196 public Boolean getFlagValue(@NonNull String flagPackageAndName) { 197 Boolean value = mFlagValues.get(flagPackageAndName); 198 Slog.d(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); 199 return value; 200 } 201 202 /** 203 * Check if the element in {@code parser} should be skipped because of the feature flag. 204 * @param parser XML parser object currently parsing an element 205 * @return true if the element is disabled because of its feature flag 206 */ skipCurrentElement(@onNull XmlResourceParser parser)207 public boolean skipCurrentElement(@NonNull XmlResourceParser parser) { 208 if (!Flags.manifestFlagging()) { 209 return false; 210 } 211 String featureFlag = parser.getAttributeValue(ANDROID_RES_NAMESPACE, "featureFlag"); 212 if (featureFlag == null) { 213 return false; 214 } 215 featureFlag = featureFlag.strip(); 216 boolean negated = false; 217 if (featureFlag.startsWith("!")) { 218 negated = true; 219 featureFlag = featureFlag.substring(1).strip(); 220 } 221 final Boolean flagValue = getFlagValue(featureFlag); 222 if (flagValue == null) { 223 Slog.w(LOG_TAG, "Skipping element " + parser.getName() 224 + " due to unknown feature flag " + featureFlag); 225 return true; 226 } 227 // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated) 228 if (flagValue == negated) { 229 Slog.v(LOG_TAG, "Skipping element " + parser.getName() 230 + " behind feature flag " + featureFlag + " = " + flagValue); 231 return true; 232 } 233 return false; 234 } 235 236 /** 237 * Add Aconfig flag values for testing flagging of manifest entries. 238 * @param flagValues A map of flag name -> value. 239 */ 240 @VisibleForTesting addFlagValuesForTesting(@onNull Map<String, Boolean> flagValues)241 public void addFlagValuesForTesting(@NonNull Map<String, Boolean> flagValues) { 242 mFlagValues.putAll(flagValues); 243 } 244 } 245