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