1 /*
2  * Copyright (C) 2023 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 android.platform.test.flag.junit.host;
18 
19 import android.aconfig.Aconfig.flag_permission;
20 import android.aconfig.Aconfig.flag_state;
21 import android.aconfig.Aconfig.parsed_flags;
22 import android.platform.test.flag.util.Flag;
23 import android.platform.test.flag.util.FlagReadException;
24 
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.device.ITestDevice;
27 import com.android.tradefed.log.LogUtil;
28 
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Map;
36 
37 import javax.annotation.Nullable;
38 
39 /**
40  * Dumps flags from a give device.
41  *
42  * <p>There are two sources to dump the flag values: device_config and aconfig dumped output. The
43  * device_config contains the ground truth for readwrite flags (including legacy flags), while the
44  * aconfig dumped output contains the ground truth for all readonly flags.
45  *
46  * <p>The {packageName}.{flagName} (we call it full flag name) is the unique identity of each
47  * aconfig flag, which is also the constant value for each flag name in the aconfig auto generated
48  * library. However, each aconfig flag definitions also contains namespace which is in aconfig
49  * dumped output. The {@code DeviceFlags} saves all flag values into a map with the key always
50  * starts with "{namespace}/" no matter it is a legacy flag or aconfig flag, and gets flag values
51  * from this map.
52  */
53 public class DeviceFlags {
54     private static final String DUMP_DEVICE_CONFIG_CMD = "device_config list";
55 
56     /**
57      * Partitions that contain the aconfig_flags.pb files. Should be consistent with the partitions
58      * defined in core/packaging/flags.mk.
59      */
60     private static final List<String> FLAG_PARTITIONS =
61             List.of("product", "system", "system_ext", "vendor");
62 
63     /**
64      * The key is the flag name with namespace ({namespace}/{flagName} for legacy flags,
65      * {namespace}/{packageName}.{flagName} for aconfig flags.
66      */
67     private final Map<String, String> mAllFlagValues = new HashMap<>();
68 
69     /**
70      * The key is the aconfig full flag name ({packageName}.{flagName}), the value is the flag name
71      * with namespace ({namespace}/{packageName}.{flagName}).
72      */
73     private final Map<String, String> mFlagNameWithNamespaces = new HashMap<>();
74 
DeviceFlags()75     private DeviceFlags() {}
76 
createDeviceFlags(ITestDevice testDevice)77     public static DeviceFlags createDeviceFlags(ITestDevice testDevice) throws FlagReadException {
78         DeviceFlags deviceFlags = new DeviceFlags();
79         deviceFlags.init(testDevice);
80         return deviceFlags;
81     }
82 
83     @Nullable
getFlagValue(String flag)84     public String getFlagValue(String flag) {
85         String flagWithNamespace =
86                 flag.contains(Flag.NAMESPACE_FLAG_SEPARATOR)
87                         ? flag
88                         : mFlagNameWithNamespaces.get(flag);
89 
90         return mAllFlagValues.get(flagWithNamespace);
91     }
92 
init(ITestDevice testDevice)93     public void init(ITestDevice testDevice) throws FlagReadException {
94         mAllFlagValues.clear();
95         mFlagNameWithNamespaces.clear();
96         LogUtil.CLog.i("Dumping all flag values from the device ...");
97         getDeviceConfigFlags(testDevice);
98         getAconfigFlags(testDevice);
99         LogUtil.CLog.i("Dumped all flag values from the device.");
100     }
101 
getDeviceConfigFlags(ITestDevice testDevice)102     private void getDeviceConfigFlags(ITestDevice testDevice) throws FlagReadException {
103         try {
104             String deviceConfigList = testDevice.executeShellCommand(DUMP_DEVICE_CONFIG_CMD);
105             for (String deviceConfig : deviceConfigList.split("\n")) {
106                 if (deviceConfig.contains("=")) {
107                     String[] flagNameValue = deviceConfig.split("=", 2);
108                     mAllFlagValues.put(flagNameValue[0], flagNameValue[1]);
109                 } else {
110                     LogUtil.CLog.w("Invalid device config %s", deviceConfig);
111                 }
112             }
113         } catch (DeviceNotAvailableException e) {
114             throw new FlagReadException("ALL_FLAGS", e);
115         }
116     }
117 
getAconfigFlags(ITestDevice testDevice)118     private void getAconfigFlags(ITestDevice testDevice) throws FlagReadException {
119         getAconfigParsedFlags(testDevice)
120                 .getParsedFlagList()
121                 .forEach(
122                         parsedFlag -> {
123                             String namespace = parsedFlag.getNamespace();
124                             String fullFlagName =
125                                     String.format(
126                                             Flag.ACONFIG_FULL_FLAG_FORMAT,
127                                             parsedFlag.getPackage(),
128                                             parsedFlag.getName());
129                             String flagWithNamespace =
130                                     String.format(
131                                             Flag.FLAG_WITH_NAMESPACE_FORMAT,
132                                             namespace,
133                                             fullFlagName);
134                             mFlagNameWithNamespaces.put(fullFlagName, flagWithNamespace);
135                             String defaultValue =
136                                     parsedFlag.getState().equals(flag_state.ENABLED)
137                                             ? "true"
138                                             : "false";
139                             if (parsedFlag.getPermission().equals(flag_permission.READ_ONLY)
140                                     || !mAllFlagValues.containsKey(flagWithNamespace)) {
141                                 mAllFlagValues.put(flagWithNamespace, defaultValue);
142                             }
143                         });
144     }
145 
getAconfigParsedFlags(ITestDevice testDevice)146     private parsed_flags getAconfigParsedFlags(ITestDevice testDevice) throws FlagReadException {
147         parsed_flags.Builder builder = parsed_flags.newBuilder();
148 
149         for (String flagPartition : FLAG_PARTITIONS) {
150             try {
151                 String aconfigFlagsPbFilePath =
152                         String.format("/%s/etc/aconfig_flags.pb", flagPartition);
153                 if (!testDevice.doesFileExist(aconfigFlagsPbFilePath)) {
154                     LogUtil.CLog.i("Aconfig flags file %s does not exist", aconfigFlagsPbFilePath);
155                     continue;
156                 }
157                 File aconfigFlagsPbFile = testDevice.pullFile(aconfigFlagsPbFilePath);
158                 if (aconfigFlagsPbFile.length() <= 1) {
159                     LogUtil.CLog.i("Aconfig flags file %s is empty", aconfigFlagsPbFilePath);
160                     continue;
161                 }
162                 InputStream aconfigFlagsPbInputStream = new FileInputStream(aconfigFlagsPbFile);
163                 parsed_flags parsedFlags = parsed_flags.parseFrom(aconfigFlagsPbInputStream);
164                 builder.addAllParsedFlag(parsedFlags.getParsedFlagList());
165             } catch (DeviceNotAvailableException | IOException e) {
166                 throw new FlagReadException("ALL_FLAGS", e);
167             }
168         }
169         return builder.build();
170     }
171 }
172