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