1 /*
2  * Copyright (C) 2011 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 package com.android.cts.tradefed.result;
17 
18 import com.android.tradefed.log.LogUtil.CLog;
19 
20 import org.kxml2.io.KXmlSerializer;
21 import org.xmlpull.v1.XmlPullParser;
22 import org.xmlpull.v1.XmlPullParserException;
23 
24 import android.tests.getinfo.DeviceInfoConstants;
25 
26 import java.io.IOException;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.Map;
31 import java.util.Set;
32 
33 /**
34  * Data structure for the device info collected by CTS.
35  * <p/>
36  * Provides methods to serialize and deserialize from XML, as well as checks for consistency
37  * when multiple devices are used to generate the report.
38  */
39 class DeviceInfoResult extends AbstractXmlPullParser {
40     static final String TAG = "DeviceInfo";
41     private static final String ns = CtsXmlResultReporter.ns;
42     static final String BUILD_TAG = "BuildInfo";
43     private static final String PHONE_TAG = "PhoneSubInfo";
44     private static final String SCREEN_TAG = "Screen";
45 
46     private static final String FEATURE_INFO_TAG = "FeatureInfo";
47     private static final String FEATURE_TAG = "Feature";
48     private static final String FEATURE_ATTR_DELIM = ":";
49     private static final String FEATURE_DELIM = ";";
50 
51     private static final String OPENGL_TEXTURE_FORMATS_INFO_TAG =
52             "OpenGLCompressedTextureFormatsInfo";
53     private static final String OPENGL_TEXTURE_FORMAT_TAG = "TextureFormat";
54     private static final String OPENGL_TEXTURE_FORMAT_DELIM = ";";
55 
56     private static final String OPENGL_EXTENSIONS_TAG = "OpenGlExtensions";
57     private static final String OPENGL_EXTENSION_TAG = "OpenGlExtension";
58     private static final String OPENGL_EXTENSION_DELIM = ";";
59 
60     private static final String SYSLIB_INFO_TAG = "SystemLibrariesInfo";
61     private static final String SYSLIB_TAG = "Library";
62     private static final String SYSLIB_DELIM = ";";
63 
64     private static final String PROCESS_INFO_TAG = "ProcessInfo";
65     private static final String PROCESS_TAG = "Process";
66     private static final String PROCESS_DELIM = ";";
67     private static final String PROCESS_ATTR_DELIM = ":";
68 
69     private Map<String, String> mMetrics = new HashMap<String, String>();
70 
71     /**
72      * Serialize this object and all its contents to XML.
73      *
74      * @param serializer
75      * @throws IOException
76      */
serialize(KXmlSerializer serializer)77     public void serialize(KXmlSerializer serializer) throws IOException {
78         serializer.startTag(ns, TAG);
79 
80         if (mMetrics.isEmpty()) {
81             // this might be expected, if device info collection was turned off
82             CLog.d("Could not find device info");
83             serializer.endTag(ns, TAG);
84             return;
85         }
86 
87         // Extract metrics that need extra handling, and then dump the remainder into BuildInfo
88         Map<String, String> metricsCopy = new HashMap<String, String>(mMetrics);
89         serializer.startTag(ns, SCREEN_TAG);
90         serializer.attribute(ns, DeviceInfoConstants.RESOLUTION,
91                 getMetric(metricsCopy, DeviceInfoConstants.RESOLUTION));
92         serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY,
93                 getMetric(metricsCopy, DeviceInfoConstants.SCREEN_DENSITY));
94         serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY_BUCKET,
95                 getMetric(metricsCopy, DeviceInfoConstants.SCREEN_DENSITY_BUCKET));
96         serializer.attribute(ns, DeviceInfoConstants.SCREEN_SIZE,
97                 getMetric(metricsCopy, DeviceInfoConstants.SCREEN_SIZE));
98         serializer.attribute(ns, DeviceInfoConstants.SMALLEST_SCREEN_WIDTH_DP,
99                 getMetric(metricsCopy, DeviceInfoConstants.SMALLEST_SCREEN_WIDTH_DP));
100         serializer.endTag(ns, SCREEN_TAG);
101 
102         serializer.startTag(ns, PHONE_TAG);
103         serializer.attribute(ns, DeviceInfoConstants.PHONE_NUMBER,
104                 getMetric(metricsCopy, DeviceInfoConstants.PHONE_NUMBER));
105         serializer.endTag(ns, PHONE_TAG);
106 
107         String featureData = getMetric(metricsCopy, DeviceInfoConstants.FEATURES);
108         String processData = getMetric(metricsCopy, DeviceInfoConstants.PROCESSES);
109         String sysLibData = getMetric(metricsCopy, DeviceInfoConstants.SYS_LIBRARIES);
110         String textureData = getMetric(metricsCopy,
111                 DeviceInfoConstants.OPEN_GL_COMPRESSED_TEXTURE_FORMATS);
112         String openGlExtensionData = getMetric(metricsCopy,
113                 DeviceInfoConstants.OPEN_GL_EXTENSIONS);
114 
115         // dump the remaining metrics without translation
116         serializer.startTag(ns, BUILD_TAG);
117         for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
118             serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
119         }
120         serializer.endTag(ns, BUILD_TAG);
121 
122         serializeFeatureInfo(serializer, featureData);
123         serializeProcessInfo(serializer, processData);
124         serializeSystemLibrariesInfo(serializer, sysLibData);
125         serializeOpenGLCompressedTextureFormatsInfo(serializer, textureData);
126         serializeOpenGLExtensions(serializer, openGlExtensionData);
127         // End
128         serializer.endTag(ns, TAG);
129     }
130 
131     /**
132      * Fetch and remove given metric from hashmap.
133      *
134      * @return the metric value or empty string if it was not present in map.
135      */
getMetric(Map<String, String> metrics, String metricName )136     private String getMetric(Map<String, String> metrics, String metricName ) {
137         String value = metrics.remove(metricName);
138         if (value == null) {
139             value = "";
140         }
141         return value;
142     }
143 
serializeFeatureInfo(KXmlSerializer serializer, String featureData)144     private void serializeFeatureInfo(KXmlSerializer serializer, String featureData)
145             throws IOException {
146         serialize(serializer, FEATURE_INFO_TAG, FEATURE_TAG, FEATURE_DELIM, FEATURE_ATTR_DELIM,
147                 featureData, "name", "type", "available");
148     }
149 
serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)150     private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)
151             throws IOException {
152         serialize(serializer, PROCESS_INFO_TAG, PROCESS_TAG, PROCESS_DELIM, PROCESS_ATTR_DELIM,
153                 rootProcesses, "name", "uid");
154     }
155 
serializeOpenGLCompressedTextureFormatsInfo(KXmlSerializer serializer, String formats)156     private void serializeOpenGLCompressedTextureFormatsInfo(KXmlSerializer serializer,
157             String formats) throws IOException {
158         serialize(serializer, OPENGL_TEXTURE_FORMATS_INFO_TAG, OPENGL_TEXTURE_FORMAT_TAG,
159                 OPENGL_TEXTURE_FORMAT_DELIM, null, formats, "name");
160     }
161 
serializeOpenGLExtensions(KXmlSerializer serializer, String extensions)162     private void serializeOpenGLExtensions(KXmlSerializer serializer, String extensions)
163             throws IOException {
164         serialize(serializer, OPENGL_EXTENSIONS_TAG, OPENGL_EXTENSION_TAG,
165                 OPENGL_EXTENSION_DELIM, null, extensions, "name");
166     }
167 
serializeSystemLibrariesInfo(KXmlSerializer serializer, String libs)168     private void serializeSystemLibrariesInfo(KXmlSerializer serializer, String libs)
169             throws IOException {
170         serialize(serializer, SYSLIB_INFO_TAG, SYSLIB_TAG, SYSLIB_DELIM, null, libs, "name");
171     }
172 
173     /**
174      * Serializes a XML structure where there is an outer tag with tags inside it.
175      *
176      * <pre>
177      *   Input: value1:value2;value3:value4
178      *
179      *   Output:
180      *   <OuterTag>
181      *     <SubTag attr1="value1" attr2="value2" />
182      *     <SubTag attr1="value3" attr2="value4" />
183      *   </OuterTag>
184      * </pre>
185      *
186      * @param serializer to do it
187      * @param tag would be "OuterTag"
188      * @param subTag would be "SubTag"
189      * @param delim would be ";"
190      * @param attrDelim would be ":" in the example but can be null if only one attrName given
191      * @param data would be "value1:value2;value3:value4"
192      * @param attrNames would be an array with "attr1", "attr2"
193      * @throws IOException if there is a problem
194      */
serialize(KXmlSerializer serializer, String tag, String subTag, String delim, String attrDelim, String data, String... attrNames)195     private void serialize(KXmlSerializer serializer, String tag, String subTag,
196             String delim, String attrDelim, String data, String... attrNames) throws IOException {
197         serializer.startTag(ns, tag);
198 
199         if (data == null) {
200             data = "";
201         }
202 
203         String[] values = data.split(delim);
204         for (String value : values) {
205             if (!value.isEmpty()) {
206                 String[] attrValues = attrDelim != null ? value.split(attrDelim) : new String[] {value};
207                 if (attrValues.length == attrNames.length) {
208                     serializer.startTag(ns, subTag);
209                     for (int i = 0; i < attrNames.length; i++) {
210                         serializer.attribute(ns, attrNames[i], attrValues[i]);
211                     }
212                     serializer.endTag(ns,  subTag);
213                 }
214             }
215         }
216 
217         serializer.endTag(ns, tag);
218     }
219 
220     /**
221      * Populates this class with package result data parsed from XML.
222      *
223      * @param parser the {@link XmlPullParser}. Expected to be pointing at start
224      *            of a {@link #TAG}
225      */
226     @Override
parse(XmlPullParser parser)227     void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
228         if (!parser.getName().equals(TAG)) {
229             throw new XmlPullParserException(String.format(
230                     "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
231         }
232         int eventType = parser.getEventType();
233         while (eventType != XmlPullParser.END_DOCUMENT) {
234             if (eventType == XmlPullParser.START_TAG) {
235                 if (parser.getName().equals(SCREEN_TAG) ||
236                         parser.getName().equals(PHONE_TAG) ||
237                         parser.getName().equals(BUILD_TAG)) {
238                     addMetricsFromAttributes(parser);
239                 } else if (parser.getName().equals(FEATURE_INFO_TAG)) {
240                     mMetrics.put(DeviceInfoConstants.FEATURES, parseFeatures(parser));
241                 } else if (parser.getName().equals(PROCESS_INFO_TAG)) {
242                     mMetrics.put(DeviceInfoConstants.PROCESSES, parseProcess(parser));
243                 } else if (parser.getName().equals(SYSLIB_INFO_TAG)) {
244                     mMetrics.put(DeviceInfoConstants.SYS_LIBRARIES, parseSystemLibraries(parser));
245                 } else if (parser.getName().equals(OPENGL_TEXTURE_FORMATS_INFO_TAG)) {
246                     mMetrics.put(DeviceInfoConstants.OPEN_GL_COMPRESSED_TEXTURE_FORMATS,
247                             parseOpenGLCompressedTextureFormats(parser));
248                 }
249             } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
250                 return;
251             }
252             eventType = parser.next();
253         }
254     }
255 
parseFeatures(XmlPullParser parser)256     private String parseFeatures(XmlPullParser parser) throws XmlPullParserException, IOException {
257         return parseTag(parser, FEATURE_INFO_TAG, FEATURE_TAG, FEATURE_DELIM, FEATURE_ATTR_DELIM,
258                 "name", "type", "available");
259     }
260 
parseProcess(XmlPullParser parser)261     private String parseProcess(XmlPullParser parser) throws XmlPullParserException, IOException {
262         return parseTag(parser, PROCESS_INFO_TAG, PROCESS_TAG, PROCESS_DELIM,
263                 PROCESS_ATTR_DELIM, "name", "uid");
264     }
265 
parseOpenGLCompressedTextureFormats(XmlPullParser parser)266     private String parseOpenGLCompressedTextureFormats(XmlPullParser parser)
267             throws XmlPullParserException, IOException {
268         return parseTag(parser, OPENGL_TEXTURE_FORMATS_INFO_TAG, OPENGL_TEXTURE_FORMAT_TAG,
269                 OPENGL_TEXTURE_FORMAT_DELIM, null, "name");
270     }
271 
parseSystemLibraries(XmlPullParser parser)272     private String parseSystemLibraries(XmlPullParser parser)
273             throws XmlPullParserException, IOException {
274         return parseTag(parser, SYSLIB_INFO_TAG, SYSLIB_TAG, SYSLIB_DELIM, null, "name");
275     }
276 
277     /**
278      * Converts XML into a flattened string.
279      *
280      * <pre>
281      *   Input:
282      *   <OuterTag>
283      *     <SubTag attr1="value1" attr2="value2" />
284      *     <SubTag attr1="value3" attr2="value4" />
285      *   </OuterTag>
286      *
287      *   Output: value1:value2;value3:value4
288      * </pre>
289      *
290      * @param parser that parses the xml
291      * @param tag like "OuterTag"
292      * @param subTag like "SubTag"
293      * @param delim like ";"
294      * @param attrDelim like ":" or null if tehre is only one attribute
295      * @param attrNames like "attr1", "attr2"
296      * @return flattened string like "value1:value2;value3:value4"
297      * @throws XmlPullParserException
298      * @throws IOException
299      */
parseTag(XmlPullParser parser, String tag, String subTag, String delim, String attrDelim, String... attrNames)300     private String parseTag(XmlPullParser parser, String tag, String subTag, String delim,
301             String attrDelim, String... attrNames) throws XmlPullParserException, IOException {
302         if (!parser.getName().equals(tag)) {
303             throw new XmlPullParserException(String.format(
304                     "invalid XML: Expected %s tag but received %s", tag,
305                     parser.getName()));
306         }
307         StringBuilder flattened = new StringBuilder();
308 
309         for (int eventType = parser.getEventType();
310                 eventType != XmlPullParser.END_DOCUMENT;
311                 eventType = parser.next()) {
312 
313             if (eventType == XmlPullParser.START_TAG && parser.getName().equals(subTag)) {
314                 for (int i = 0; i < attrNames.length; i++) {
315                     flattened.append(getAttribute(parser, attrNames[i]));
316                     if (i + 1 < attrNames.length) {
317                         flattened.append(attrDelim);
318                     }
319                 }
320                 flattened.append(delim);
321             } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(tag)) {
322                 break;
323             }
324         }
325 
326         return flattened.toString();
327     }
328 
329     /**
330      * Adds all attributes from the current XML tag to metrics as name-value pairs
331      */
addMetricsFromAttributes(XmlPullParser parser)332     private void addMetricsFromAttributes(XmlPullParser parser) {
333         int attrCount = parser.getAttributeCount();
334         for (int i = 0; i < attrCount; i++) {
335             mMetrics.put(parser.getAttributeName(i), parser.getAttributeValue(i));
336         }
337     }
338 
339     /**
340      * Populate the device info metrics with values collected from device.
341      * <p/>
342      * Check that the provided device info metrics are consistent with the currently stored metrics.
343      * If any inconsistencies occur, logs errors and stores error messages in the metrics map
344      */
populateMetrics(Map<String, String> metrics)345     public void populateMetrics(Map<String, String> metrics) {
346         if (mMetrics.isEmpty()) {
347             // no special processing needed, no existing metrics
348             mMetrics.putAll(metrics);
349             return;
350         }
351         Map<String, String> metricsCopy = new HashMap<String, String>(
352                 metrics);
353         // add values for metrics that might be different across runs
354         combineMetrics(metricsCopy, DeviceInfoConstants.PHONE_NUMBER, DeviceInfoConstants.IMSI,
355                 DeviceInfoConstants.IMSI, DeviceInfoConstants.SERIAL_NUMBER);
356 
357         // ensure all the metrics we expect to be identical actually are
358         checkMetrics(metricsCopy, DeviceInfoConstants.BUILD_FINGERPRINT,
359                 DeviceInfoConstants.BUILD_MODEL, DeviceInfoConstants.BUILD_BRAND,
360                 DeviceInfoConstants.BUILD_MANUFACTURER, DeviceInfoConstants.BUILD_BOARD,
361                 DeviceInfoConstants.BUILD_DEVICE, DeviceInfoConstants.PRODUCT_NAME,
362                 DeviceInfoConstants.BUILD_ABI, DeviceInfoConstants.BUILD_ABI2,
363                 DeviceInfoConstants.BUILD_ABIS, DeviceInfoConstants.BUILD_ABIS_32,
364                 DeviceInfoConstants.BUILD_ABIS_64, DeviceInfoConstants.SCREEN_SIZE);
365     }
366 
combineMetrics(Map<String, String> metrics, String... keysToCombine)367     private void combineMetrics(Map<String, String> metrics, String... keysToCombine) {
368         for (String combineKey : keysToCombine) {
369             String currentKeyValue = mMetrics.get(combineKey);
370             String valueToAdd = metrics.remove(combineKey);
371             if (valueToAdd != null) {
372                 if (currentKeyValue == null) {
373                     // strange - no existing value. Can occur during unit testing
374                     mMetrics.put(combineKey, valueToAdd);
375                 } else if (!currentKeyValue.equals(valueToAdd)) {
376                     // new value! store a comma separated list
377                     valueToAdd = String.format("%s,%s", currentKeyValue, valueToAdd);
378                     mMetrics.put(combineKey, valueToAdd);
379                 } else {
380                     // ignore, current value is same as existing
381                 }
382 
383             } else {
384                 CLog.d("Missing metric %s", combineKey);
385             }
386         }
387     }
388 
checkMetrics(Map<String, String> metrics, String... keysToCheck)389     private void checkMetrics(Map<String, String> metrics, String... keysToCheck) {
390         Set<String> keyCheckSet = new HashSet<String>();
391         Collections.addAll(keyCheckSet, keysToCheck);
392         for (Map.Entry<String, String> metricEntry : metrics.entrySet()) {
393             String currentValue = mMetrics.get(metricEntry.getKey());
394             if (keyCheckSet.contains(metricEntry.getKey()) && currentValue != null
395                     && !metricEntry.getValue().equals(currentValue)) {
396                 CLog.e("Inconsistent info collected from devices. "
397                         + "Current result has %s='%s', Received '%s'. Are you sharding or " +
398                         "resuming a test run across different devices and/or builds?",
399                         metricEntry.getKey(), currentValue, metricEntry.getValue());
400                 mMetrics.put(metricEntry.getKey(),
401                         String.format("ERROR: Inconsistent results: %s, %s",
402                                 metricEntry.getValue(), currentValue));
403             } else {
404                 mMetrics.put(metricEntry.getKey(), metricEntry.getValue());
405             }
406         }
407     }
408 
409     /**
410      * Return the currently stored metrics.
411      * <p/>
412      * Exposed for unit testing.
413      */
getMetrics()414     Map<String, String> getMetrics() {
415         return mMetrics;
416     }
417 }
418