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