Merge "Support merging device info metrics." into ics-mr0
diff --git a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoConstants.java b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoConstants.java
index bfe5aaa..57b0dad 100644
--- a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoConstants.java
+++ b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoConstants.java
@@ -40,8 +40,7 @@
     public static final String SCREEN_SIZE = "screen_size";
     public static final String SCREEN_DENSITY_BUCKET = "screen_density_bucket";
     public static final String SCREEN_DENSITY = "screen_density";
-    public static final String SCREEN_HEIGHT = "screen_height";
-    public static final String SCREEN_WIDTH = "screen_width";
+    public static final String RESOLUTION = "resolution";
     public static final String VERSION_SDK = "androidPlatformVersion";
     public static final String VERSION_RELEASE = "buildVersion";
     public static final String BUILD_ABI = "build_abi";
@@ -57,5 +56,5 @@
     public static final String BUILD_ID = "buildID";
     public static final String BUILD_VERSION = "buildVersion";
     public static final String BUILD_TAGS = "build_tags";
-    public static final String SERIAL_NUMBER = "serialNumber";
+    public static final String SERIAL_NUMBER = "deviceID";
 }
diff --git a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
index 5ab063d..1b705e7 100644
--- a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
+++ b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
@@ -68,6 +68,7 @@
         addResult(BUILD_FINGERPRINT, Build.FINGERPRINT);
         addResult(BUILD_ABI, Build.CPU_ABI);
         addResult(BUILD_ABI2, Build.CPU_ABI2);
+        addResult(SERIAL_NUMBER, Build.SERIAL);
 
         addResult(VERSION_RELEASE, Build.VERSION.RELEASE);
         addResult(VERSION_SDK, Build.VERSION.SDK);
@@ -77,8 +78,7 @@
                 Context.WINDOW_SERVICE);
         Display d = wm.getDefaultDisplay();
         d.getMetrics(metrics);
-        addResult(SCREEN_WIDTH, metrics.widthPixels);
-        addResult(SCREEN_HEIGHT, metrics.heightPixels);
+        addResult(RESOLUTION, String.format("%sx%s", metrics.widthPixels, metrics.heightPixels));
         addResult(SCREEN_DENSITY, metrics.density);
         addResult(SCREEN_X_DENSITY, metrics.xdpi);
         addResult(SCREEN_Y_DENSITY, metrics.ydpi);
diff --git a/tools/host/src/com/android/cts/TestDevice.java b/tools/host/src/com/android/cts/TestDevice.java
index a90f6f1..dcedc02 100644
--- a/tools/host/src/com/android/cts/TestDevice.java
+++ b/tools/host/src/com/android/cts/TestDevice.java
@@ -682,7 +682,7 @@
          * @return The screen resolution.
          */
         public String getScreenResolution() {
-            return mInfoMap.get(SCREEN_WIDTH) + "x" + mInfoMap.get(SCREEN_HEIGHT);
+            return mInfoMap.get(RESOLUTION);
         }
 
         /**
diff --git a/tools/tradefed-host/.classpath b/tools/tradefed-host/.classpath
index 335f3c3..197543c 100644
--- a/tools/tradefed-host/.classpath
+++ b/tools/tradefed-host/.classpath
@@ -7,6 +7,6 @@
 	<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/tradefederation"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/hosttestlib"/>
-	<classpathentry combineaccessrules="false" kind="src" path="/ctsdeviceinfolib"/>
+	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/ctsdeviceinfolib"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
index 2893523..11b4b1c 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
@@ -16,10 +16,7 @@
 
 package com.android.cts.tradefed.result;
 
-import android.tests.getinfo.DeviceInfoConstants;
-
 import com.android.cts.tradefed.build.CtsBuildHelper;
-import com.android.cts.tradefed.build.CtsBuildProvider;
 import com.android.cts.tradefed.device.DeviceInfoCollector;
 import com.android.cts.tradefed.testtype.CtsTest;
 import com.android.ddmlib.Log;
@@ -46,9 +43,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -70,12 +64,6 @@
     /** the XML namespace */
     static final String ns = null;
 
-    // XML constants
-    static final String SUMMARY_TAG = "Summary";
-    static final String PASS_ATTR = "pass";
-    static final String TIMEOUT_ATTR = "timeout";
-    static final String NOT_EXECUTED_ATTR = "notExecuted";
-    static final String FAILED_ATTR = "failed";
     static final String RESULT_TAG = "TestResult";
     static final String PLAN_ATTR = "testPlan";
 
@@ -101,6 +89,7 @@
     private String mDeviceSerial;
     private TestResults mResults = new TestResults();
     private TestPackageResult mCurrentPkgResult = null;
+    private boolean mIsDeviceInfoRun = false;
 
     public void setReportDir(File reportDir) {
         mReportDir = reportDir;
@@ -189,15 +178,18 @@
             // display results from previous run
             logCompleteRun(mCurrentPkgResult);
         }
-        if (name.equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
+        mIsDeviceInfoRun = name.equals(DeviceInfoCollector.APP_PACKAGE_NAME);
+        if (mIsDeviceInfoRun) {
             logResult("Collecting device info");
-        } else  if (mCurrentPkgResult == null || !name.equals(
-                mCurrentPkgResult.getAppPackageName())) {
-            logResult("-----------------------------------------");
-            logResult("Test package %s started", name);
-            logResult("-----------------------------------------");
+        } else  {
+            if (mCurrentPkgResult == null || !name.equals(mCurrentPkgResult.getAppPackageName())) {
+                logResult("-----------------------------------------");
+                logResult("Test package %s started", name);
+                logResult("-----------------------------------------");
+            }
+            mCurrentPkgResult = mResults.getOrCreatePackage(name);
         }
-        mCurrentPkgResult = mResults.getOrCreatePackage(name);
+
     }
 
     /**
@@ -233,7 +225,11 @@
      */
     @Override
     public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
-        mCurrentPkgResult.populateMetrics(runMetrics);
+        if (mIsDeviceInfoRun) {
+            mResults.populateDeviceInfoMetrics(runMetrics);
+        } else {
+            mCurrentPkgResult.populateMetrics(runMetrics);
+        }
     }
 
     /**
@@ -317,194 +313,12 @@
         serializer.attribute(ns, "endtime", endTime);
         serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION);
 
-        serializeDeviceInfo(serializer);
-        serializeHostInfo(serializer);
-        serializeTestSummary(serializer);
         mResults.serialize(serializer);
         // TODO: not sure why, but the serializer doesn't like this statement
         //serializer.endTag(ns, RESULT_TAG);
     }
 
     /**
-     * Output the device info XML.
-     *
-     * @param serializer
-     */
-    private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException {
-        serializer.startTag(ns, "DeviceInfo");
-
-        Map<String, String> deviceInfoMetrics = mResults.getDeviceInfoMetrics();
-        if (deviceInfoMetrics == null || deviceInfoMetrics.isEmpty()) {
-            // this might be expected, if device info collection was turned off
-            CLog.d("Could not find device info");
-            return;
-        }
-
-        // Extract metrics that need extra handling, and then dump the remainder into BuildInfo
-        Map<String, String> metricsCopy = new HashMap<String, String>(
-                deviceInfoMetrics);
-        serializer.startTag(ns, "Screen");
-        String screenWidth = metricsCopy.remove(DeviceInfoConstants.SCREEN_WIDTH);
-        String screenHeight = metricsCopy.remove(DeviceInfoConstants.SCREEN_HEIGHT);
-        serializer.attribute(ns, "resolution", String.format("%sx%s", screenWidth, screenHeight));
-        serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY,
-                metricsCopy.remove(DeviceInfoConstants.SCREEN_DENSITY));
-        serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY_BUCKET,
-                metricsCopy.remove(DeviceInfoConstants.SCREEN_DENSITY_BUCKET));
-        serializer.attribute(ns, DeviceInfoConstants.SCREEN_SIZE,
-                metricsCopy.remove(DeviceInfoConstants.SCREEN_SIZE));
-        serializer.endTag(ns, "Screen");
-
-        serializer.startTag(ns, "PhoneSubInfo");
-        serializer.attribute(ns, "subscriberId", metricsCopy.remove(
-                DeviceInfoConstants.PHONE_NUMBER));
-        serializer.endTag(ns, "PhoneSubInfo");
-
-        String featureData = metricsCopy.remove(DeviceInfoConstants.FEATURES);
-        String processData = metricsCopy.remove(DeviceInfoConstants.PROCESSES);
-
-        // dump the remaining metrics without translation
-        serializer.startTag(ns, "BuildInfo");
-        for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
-            serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
-        }
-        serializer.attribute(ns, "deviceID", mDeviceSerial);
-        serializer.endTag(ns, "BuildInfo");
-
-        serializeFeatureInfo(serializer, featureData);
-        serializeProcessInfo(serializer, processData);
-
-        serializer.endTag(ns, "DeviceInfo");
-    }
-
-    /**
-     * Prints XML indicating what features are supported by the device. It parses a string from the
-     * featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;"
-     * with a trailing semi-colon.
-     *
-     * <pre>
-     *  <FeatureInfo>
-     *     <Feature name="android.name.of.feature" available="true" />
-     *     ...
-     *   </FeatureInfo>
-     * </pre>
-     *
-     * @param serializer used to create XML
-     * @param featureData raw unparsed feature data
-     */
-    private void serializeFeatureInfo(KXmlSerializer serializer, String featureData) throws IOException {
-        serializer.startTag(ns, "FeatureInfo");
-
-        if (featureData == null) {
-            featureData = "";
-        }
-
-        String[] featurePairs = featureData.split(";");
-        for (String featurePair : featurePairs) {
-            String[] nameTypeAvailability = featurePair.split(":");
-            if (nameTypeAvailability.length >= 3) {
-                serializer.startTag(ns, "Feature");
-                serializer.attribute(ns, "name", nameTypeAvailability[0]);
-                serializer.attribute(ns, "type", nameTypeAvailability[1]);
-                serializer.attribute(ns, "available", nameTypeAvailability[2]);
-                serializer.endTag(ns, "Feature");
-            }
-        }
-        serializer.endTag(ns, "FeatureInfo");
-    }
-
-    /**
-     * Prints XML data indicating what particular processes of interest were running on the device.
-     * It parses a string from the rootProcesses argument that is in the form of
-     * "processName1;processName2;..." with a trailing semi-colon.
-     *
-     * <pre>
-     *   <ProcessInfo>
-     *     <Process name="long_cat_viewer" uid="0" />
-     *     ...
-     *   </ProcessInfo>
-     * </pre>
-     */
-    private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)
-            throws IOException {
-        serializer.startTag(ns, "ProcessInfo");
-
-        if (rootProcesses == null) {
-            rootProcesses = "";
-        }
-
-        String[] processNames = rootProcesses.split(";");
-        for (String processName : processNames) {
-            processName = processName.trim();
-            if (processName.length() > 0) {
-                serializer.startTag(ns, "Process");
-                serializer.attribute(ns, "name", processName);
-                serializer.attribute(ns, "uid", "0");
-                serializer.endTag(ns, "Process");
-            }
-        }
-        serializer.endTag(ns, "ProcessInfo");
-    }
-
-    /**
-     * Output the host info XML.
-     *
-     * @param serializer
-     */
-    private void serializeHostInfo(KXmlSerializer serializer) throws IOException {
-        serializer.startTag(ns, "HostInfo");
-
-        String hostName = "";
-        try {
-            hostName = InetAddress.getLocalHost().getHostName();
-        } catch (UnknownHostException ignored) {}
-        serializer.attribute(ns, "name", hostName);
-
-        serializer.startTag(ns, "Os");
-        serializer.attribute(ns, "name", System.getProperty("os.name"));
-        serializer.attribute(ns, "version", System.getProperty("os.version"));
-        serializer.attribute(ns, "arch", System.getProperty("os.arch"));
-        serializer.endTag(ns, "Os");
-
-        serializer.startTag(ns, "Java");
-        serializer.attribute(ns, "name", System.getProperty("java.vendor"));
-        serializer.attribute(ns, "version", System.getProperty("java.version"));
-        serializer.endTag(ns, "Java");
-
-        serializer.startTag(ns, "Cts");
-        serializer.attribute(ns, "version", CtsBuildProvider.CTS_BUILD_VERSION);
-        // TODO: consider outputting other tradefed options here
-        serializer.startTag(ns, "IntValue");
-        serializer.attribute(ns, "name", "testStatusTimeoutMs");
-        // TODO: create a constant variable for testStatusTimeoutMs value. Currently it cannot be
-        // changed
-        serializer.attribute(ns, "value", "600000");
-        serializer.endTag(ns, "IntValue");
-        serializer.endTag(ns, "Cts");
-
-        serializer.endTag(ns, "HostInfo");
-    }
-
-    /**
-     * Output the test summary XML containing summary totals for all tests.
-     *
-     * @param serializer
-     * @throws IOException
-     */
-    private void serializeTestSummary(KXmlSerializer serializer) throws IOException {
-        serializer.startTag(ns, SUMMARY_TAG);
-        serializer.attribute(ns, FAILED_ATTR, Integer.toString(mResults.countTests(
-                CtsTestStatus.FAIL)));
-        serializer.attribute(ns, NOT_EXECUTED_ATTR,  Integer.toString(mResults.countTests(
-                CtsTestStatus.NOT_EXECUTED)));
-        // ignore timeouts - these are reported as errors
-        serializer.attribute(ns, TIMEOUT_ATTR, "0");
-        serializer.attribute(ns, PASS_ATTR, Integer.toString(mResults.countTests(
-                CtsTestStatus.PASS)));
-        serializer.endTag(ns, SUMMARY_TAG);
-    }
-
-    /**
      * Creates the output stream to use for test results. Exposed for mocking.
      */
     OutputStream createOutputResultStream(File reportDir) throws IOException {
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/DeviceInfoResult.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/DeviceInfoResult.java
new file mode 100644
index 0000000..102e998
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/DeviceInfoResult.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.tradefed.result;
+
+import android.tests.getinfo.DeviceInfoConstants;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import org.kxml2.io.KXmlSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Data structure for the device info collected by CTS.
+ * <p/>
+ * Provides methods to serialize and deserialize from XML, as well as checks for consistency
+ * when multiple devices are used to generate the report.
+ */
+class DeviceInfoResult extends AbstractXmlPullParser {
+    static final String TAG = "DeviceInfo";
+    private static final String ns = CtsXmlResultReporter.ns;
+    private static final String BUILD_TAG = "BuildInfo";
+    private static final String PHONE_TAG = "PhoneSubInfo";
+    private static final String SCREEN_TAG = "Screen";
+    private static final String FEATURE_INFO_TAG = "FeatureInfo";
+    private static final String FEATURE_TAG = "Feature";
+    private static final String FEATURE_ATTR_DELIM = ":";
+    private static final String FEATURE_DELIM = ";";
+    private static final String PROCESS_INFO_TAG = "ProcessInfo";
+    private static final String PROCESS_TAG = "Process";
+    private static final String PROCESS_DELIM = ";";
+
+    private Map<String, String> mMetrics = new HashMap<String, String>();
+
+    /**
+     * Serialize this object and all its contents to XML.
+     *
+     * @param serializer
+     * @throws IOException
+     */
+    public void serialize(KXmlSerializer serializer) throws IOException {
+        serializer.startTag(ns, TAG);
+
+        if (!mMetrics.isEmpty()) {
+
+            // Extract metrics that need extra handling, and then dump the remainder into BuildInfo
+            Map<String, String> metricsCopy = new HashMap<String, String>(mMetrics);
+            serializer.startTag(ns, SCREEN_TAG);
+            serializer.attribute(ns, DeviceInfoConstants.RESOLUTION,
+                    getMetric(metricsCopy, DeviceInfoConstants.RESOLUTION));
+            serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY,
+                    getMetric(metricsCopy, DeviceInfoConstants.SCREEN_DENSITY));
+            serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY_BUCKET,
+                    getMetric(metricsCopy, DeviceInfoConstants.SCREEN_DENSITY_BUCKET));
+            serializer.attribute(ns, DeviceInfoConstants.SCREEN_SIZE,
+                    getMetric(metricsCopy, DeviceInfoConstants.SCREEN_SIZE));
+            serializer.endTag(ns, SCREEN_TAG);
+
+            serializer.startTag(ns, PHONE_TAG);
+            serializer.attribute(ns, DeviceInfoConstants.PHONE_NUMBER,
+                    getMetric(metricsCopy, DeviceInfoConstants.PHONE_NUMBER));
+            serializer.endTag(ns, PHONE_TAG);
+
+            String featureData = getMetric(metricsCopy, DeviceInfoConstants.FEATURES);
+            String processData = getMetric(metricsCopy, DeviceInfoConstants.PROCESSES);
+
+            // dump the remaining metrics without translation
+            serializer.startTag(ns, BUILD_TAG);
+            for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
+                serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
+            }
+            serializer.endTag(ns, BUILD_TAG);
+
+            serializeFeatureInfo(serializer, featureData);
+            serializeProcessInfo(serializer, processData);
+        } else {
+            // this might be expected, if device info collection was turned off
+            CLog.d("Could not find device info");
+        }
+        serializer.endTag(ns, TAG);
+    }
+
+    /**
+     * Fetch and remove given metric from hashmap.
+     *
+     * @return the metric value or empty string if it was not present in map.
+     */
+    private String getMetric(Map<String, String> metrics, String metricName ) {
+        String value = metrics.remove(metricName);
+        if (value == null) {
+            value = "";
+        }
+        return value;
+    }
+
+    /**
+     * Prints XML indicating what features are supported by the device. It parses a string from the
+     * featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;"
+     * with a trailing semi-colon.
+     *
+     * <pre>
+     *  <FeatureInfo>
+     *     <Feature name="android.name.of.feature" available="true" />
+     *     ...
+     *   </FeatureInfo>
+     * </pre>
+     *
+     * @param serializer used to create XML
+     * @param featureData raw unparsed feature data
+     */
+    private void serializeFeatureInfo(KXmlSerializer serializer, String featureData)
+            throws IOException {
+        serializer.startTag(ns, FEATURE_INFO_TAG);
+
+        if (featureData == null) {
+            featureData = "";
+        }
+
+        String[] featurePairs = featureData.split(FEATURE_DELIM);
+        for (String featurePair : featurePairs) {
+            String[] nameTypeAvailability = featurePair.split(FEATURE_ATTR_DELIM);
+            if (nameTypeAvailability.length >= 3) {
+                serializer.startTag(ns, FEATURE_TAG);
+                serializer.attribute(ns, "name", nameTypeAvailability[0]);
+                serializer.attribute(ns, "type", nameTypeAvailability[1]);
+                serializer.attribute(ns, "available", nameTypeAvailability[2]);
+                serializer.endTag(ns, FEATURE_TAG);
+            }
+        }
+        serializer.endTag(ns, FEATURE_INFO_TAG);
+    }
+
+    /**
+     * Prints XML data indicating what particular processes of interest were running on the device.
+     * It parses a string from the rootProcesses argument that is in the form of
+     * "processName1;processName2;..." with a trailing semi-colon.
+     *
+     * <pre>
+     *   <ProcessInfo>
+     *     <Process name="long_cat_viewer" uid="0" />
+     *     ...
+     *   </ProcessInfo>
+     * </pre>
+     */
+    private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)
+            throws IOException {
+        serializer.startTag(ns, PROCESS_INFO_TAG);
+
+        if (rootProcesses == null) {
+            rootProcesses = "";
+        }
+
+        String[] processNames = rootProcesses.split(PROCESS_DELIM);
+        for (String processName : processNames) {
+            processName = processName.trim();
+            if (processName.length() > 0) {
+                serializer.startTag(ns, PROCESS_TAG);
+                serializer.attribute(ns, "name", processName);
+                serializer.attribute(ns, "uid", "0");
+                serializer.endTag(ns, PROCESS_TAG);
+            }
+        }
+        serializer.endTag(ns, PROCESS_INFO_TAG);
+    }
+
+    /**
+     * Populates this class with package result data parsed from XML.
+     *
+     * @param parser the {@link XmlPullParser}. Expected to be pointing at start
+     *            of a {@link #TAG}
+     */
+    @Override
+    void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
+        }
+        int eventType = parser.getEventType();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals(SCREEN_TAG) ||
+                        parser.getName().equals(PHONE_TAG) ||
+                        parser.getName().equals(BUILD_TAG)) {
+                    addMetricsFromAttributes(parser);
+                } else if (parser.getName().equals(FEATURE_INFO_TAG)) {
+                    // store features into metrics map, in the same format as when collected from
+                    // device
+                    mMetrics.put(DeviceInfoConstants.FEATURES, parseFeatures(parser));
+                } else if (parser.getName().equals(PROCESS_INFO_TAG)) {
+                    // store processes into metrics map, in the same format as when collected from
+                    // device
+                    mMetrics.put(DeviceInfoConstants.PROCESSES, parseProcess(parser));
+                }
+            } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
+                return;
+            }
+            eventType = parser.next();
+        }
+    }
+
+    /**
+     * Parse process XML, and return its contents as a delimited String
+     */
+    private String parseProcess(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(PROCESS_INFO_TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", PROCESS_INFO_TAG,
+                    parser.getName()));
+        }
+        StringBuilder processString = new StringBuilder();
+        int eventType = parser.getEventType();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(PROCESS_TAG)) {
+                processString.append(getAttribute(parser, "name"));
+                processString.append(PROCESS_DELIM);
+            } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(
+                    PROCESS_INFO_TAG)) {
+                return processString.toString();
+            }
+            eventType = parser.next();
+        }
+        return processString.toString();
+    }
+
+    /**
+     * Parse feature XML, and return its contents as a delimited String
+     */
+    private String parseFeatures(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(FEATURE_INFO_TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", FEATURE_INFO_TAG,
+                    parser.getName()));
+        }
+        StringBuilder featureString = new StringBuilder();
+        int eventType = parser.getEventType();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(FEATURE_TAG)) {
+                featureString.append(getAttribute(parser, "name"));
+                featureString.append(FEATURE_ATTR_DELIM);
+                featureString.append(getAttribute(parser, "type"));
+                featureString.append(FEATURE_ATTR_DELIM);
+                featureString.append(getAttribute(parser, "available"));
+                featureString.append(FEATURE_DELIM);
+            } else if (eventType == XmlPullParser.END_TAG
+                    && parser.getName().equals(FEATURE_INFO_TAG)) {
+                return featureString.toString();
+            }
+            eventType = parser.next();
+        }
+        return featureString.toString();
+
+    }
+
+    /**
+     * Adds all attributes from the current XML tag to metrics as name-value pairs
+     */
+    private void addMetricsFromAttributes(XmlPullParser parser) {
+        int attrCount = parser.getAttributeCount();
+        for (int i = 0; i < attrCount; i++) {
+            mMetrics.put(parser.getAttributeName(i), parser.getAttributeValue(i));
+        }
+    }
+
+    /**
+     * Populate the device info metrics with values collected from device.
+     * <p/>
+     * Check that the provided device info metrics are consistent with the currently stored metrics.
+     * If any inconsistencies occur, logs errors and stores error messages in the metrics map
+     *
+     * @param runResult
+     */
+    public void populateMetrics(Map<String, String> metrics) {
+        if (mMetrics.isEmpty()) {
+            // no special processing needed, no existing metrics
+            mMetrics.putAll(metrics);
+            return;
+        }
+        Map<String, String> metricsCopy = new HashMap<String, String>(
+                metrics);
+        // add values for metrics that might be different across runs
+        combineMetrics(metricsCopy, DeviceInfoConstants.PHONE_NUMBER, DeviceInfoConstants.IMSI,
+                DeviceInfoConstants.IMSI, DeviceInfoConstants.SERIAL_NUMBER);
+
+        // ensure all the metrics we expect to be identical actually are
+        checkMetrics(metricsCopy, DeviceInfoConstants.BUILD_FINGERPRINT,
+                DeviceInfoConstants.BUILD_MODEL, DeviceInfoConstants.BUILD_BRAND,
+                DeviceInfoConstants.BUILD_MANUFACTURER, DeviceInfoConstants.BUILD_BOARD,
+                DeviceInfoConstants.BUILD_DEVICE, DeviceInfoConstants.PRODUCT_NAME,
+                DeviceInfoConstants.BUILD_ABI, DeviceInfoConstants.BUILD_ABI2,
+                DeviceInfoConstants.SCREEN_SIZE);
+    }
+
+    private void combineMetrics(Map<String, String> metrics, String... keysToCombine) {
+        for (String combineKey : keysToCombine) {
+            String currentKeyValue = mMetrics.get(combineKey);
+            String valueToAdd = metrics.remove(combineKey);
+            if (valueToAdd != null) {
+                if (currentKeyValue == null) {
+                    // strange - no existing value. Can occur during unit testing
+                    mMetrics.put(combineKey, valueToAdd);
+                } else if (!currentKeyValue.equals(valueToAdd)) {
+                    // new value! store a comma separated list
+                    valueToAdd = String.format("%s,%s", currentKeyValue, valueToAdd);
+                    mMetrics.put(combineKey, valueToAdd);
+                } else {
+                    // ignore, current value is same as existing
+                }
+
+            } else {
+                CLog.d("Missing metric %s", combineKey);
+            }
+        }
+    }
+
+    private void checkMetrics(Map<String, String> metrics, String... keysToCheck) {
+        Set<String> keyCheckSet = new HashSet<String>();
+        Collections.addAll(keyCheckSet, keysToCheck);
+        for (Map.Entry<String, String> metricEntry : metrics.entrySet()) {
+            String currentValue = mMetrics.get(metricEntry.getKey());
+            if (keyCheckSet.contains(metricEntry.getKey()) && currentValue != null
+                    && !metricEntry.getValue().equals(currentValue)) {
+                CLog.e("Inconsistent info collected from devices. "
+                        + "Current result has %s='%s', Received '%s'. Are you sharding or " +
+                        "resuming a test run across different devices and/or builds?",
+                        metricEntry.getKey(), currentValue, metricEntry.getValue());
+                mMetrics.put(metricEntry.getKey(),
+                        String.format("ERROR: Inconsistent results: %s, %s",
+                                metricEntry.getValue(), currentValue));
+            } else {
+                mMetrics.put(metricEntry.getKey(), metricEntry.getValue());
+            }
+        }
+    }
+
+    /**
+     * Return the currently stored metrics.
+     * <p/>
+     * Exposed for unit testing.
+     */
+    Map<String, String> getMetrics() {
+        return mMetrics;
+    }
+}
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
index a874227..2f9eadd 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
@@ -15,7 +15,7 @@
  */
 package com.android.cts.tradefed.result;
 
-import com.android.cts.tradefed.device.DeviceInfoCollector;
+import com.android.cts.tradefed.build.CtsBuildProvider;
 import com.android.tradefed.log.LogUtil.CLog;
 
 import org.kxml2.io.KXmlSerializer;
@@ -23,6 +23,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -38,8 +40,18 @@
  */
 class TestResults extends AbstractXmlPullParser {
 
-    private Map<String, TestPackageResult> mPackageMap = new LinkedHashMap<String, TestPackageResult>();
-    private TestPackageResult mDeviceInfoPkg = new TestPackageResult();
+    private static final String ns = CtsXmlResultReporter.ns;
+
+    // XML constants
+    static final String SUMMARY_TAG = "Summary";
+    static final String PASS_ATTR = "pass";
+    static final String TIMEOUT_ATTR = "timeout";
+    static final String NOT_EXECUTED_ATTR = "notExecuted";
+    static final String FAILED_ATTR = "failed";
+
+    private Map<String, TestPackageResult> mPackageMap =
+            new LinkedHashMap<String, TestPackageResult>();
+    private DeviceInfoResult mDeviceInfo = new DeviceInfoResult();
 
     /**
      * {@inheritDoc}
@@ -49,6 +61,10 @@
         int eventType = parser.getEventType();
         while (eventType != XmlPullParser.END_DOCUMENT) {
             if (eventType == XmlPullParser.START_TAG && parser.getName().equals(
+                    DeviceInfoResult.TAG)) {
+                mDeviceInfo.parse(parser);
+            }
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(
                     TestPackageResult.TAG)) {
                 TestPackageResult pkg = new TestPackageResult();
                 pkg.parse(parser);
@@ -83,24 +99,15 @@
     }
 
     /**
-     * @return
-     */
-    public Map<String, String> getDeviceInfoMetrics() {
-        return mDeviceInfoPkg.getMetrics();
-    }
-
-    /**
-     * @param mCurrentPkgResult
-     */
-    public void addPackageResult(TestPackageResult pkgResult) {
-        mPackageMap.put(pkgResult.getName(), pkgResult);
-    }
-
-    /**
+     * Serialize the test results to XML.
+     *
      * @param serializer
      * @throws IOException
      */
     public void serialize(KXmlSerializer serializer) throws IOException {
+        mDeviceInfo.serialize(serializer);
+        serializeHostInfo(serializer);
+        serializeTestSummary(serializer);
         // sort before serializing
         List<TestPackageResult> pkgs = new ArrayList<TestPackageResult>(mPackageMap.values());
         Collections.sort(pkgs, new PkgComparator());
@@ -109,6 +116,62 @@
         }
     }
 
+    /**
+     * Output the host info XML.
+     *
+     * @param serializer
+     */
+    private void serializeHostInfo(KXmlSerializer serializer) throws IOException {
+        serializer.startTag(ns, "HostInfo");
+
+        String hostName = "";
+        try {
+            hostName = InetAddress.getLocalHost().getHostName();
+        } catch (UnknownHostException ignored) {}
+        serializer.attribute(ns, "name", hostName);
+
+        serializer.startTag(ns, "Os");
+        serializer.attribute(ns, "name", System.getProperty("os.name"));
+        serializer.attribute(ns, "version", System.getProperty("os.version"));
+        serializer.attribute(ns, "arch", System.getProperty("os.arch"));
+        serializer.endTag(ns, "Os");
+
+        serializer.startTag(ns, "Java");
+        serializer.attribute(ns, "name", System.getProperty("java.vendor"));
+        serializer.attribute(ns, "version", System.getProperty("java.version"));
+        serializer.endTag(ns, "Java");
+
+        serializer.startTag(ns, "Cts");
+        serializer.attribute(ns, "version", CtsBuildProvider.CTS_BUILD_VERSION);
+        // TODO: consider outputting other tradefed options here
+        serializer.startTag(ns, "IntValue");
+        serializer.attribute(ns, "name", "testStatusTimeoutMs");
+        // TODO: create a constant variable for testStatusTimeoutMs value. Currently it cannot be
+        // changed
+        serializer.attribute(ns, "value", "600000");
+        serializer.endTag(ns, "IntValue");
+        serializer.endTag(ns, "Cts");
+
+        serializer.endTag(ns, "HostInfo");
+    }
+
+    /**
+     * Output the test summary XML containing summary totals for all tests.
+     *
+     * @param serializer
+     * @throws IOException
+     */
+    private void serializeTestSummary(KXmlSerializer serializer) throws IOException {
+        serializer.startTag(ns, SUMMARY_TAG);
+        serializer.attribute(ns, FAILED_ATTR, Integer.toString(countTests(CtsTestStatus.FAIL)));
+        serializer.attribute(ns, NOT_EXECUTED_ATTR,
+                Integer.toString(countTests(CtsTestStatus.NOT_EXECUTED)));
+        // ignore timeouts - these are reported as errors
+        serializer.attribute(ns, TIMEOUT_ATTR, "0");
+        serializer.attribute(ns, PASS_ATTR, Integer.toString(countTests(CtsTestStatus.PASS)));
+        serializer.endTag(ns, SUMMARY_TAG);
+    }
+
     private static class PkgComparator implements Comparator<TestPackageResult> {
 
         /**
@@ -127,10 +190,6 @@
      * @return
      */
     public TestPackageResult getOrCreatePackage(String appPackageName) {
-        if (appPackageName.equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
-            mDeviceInfoPkg.setAppPackageName(DeviceInfoCollector.APP_PACKAGE_NAME);
-            return mDeviceInfoPkg ;
-        }
         TestPackageResult pkgResult = mPackageMap.get(appPackageName);
         if (pkgResult == null) {
             pkgResult = new TestPackageResult();
@@ -139,4 +198,12 @@
         }
         return pkgResult;
     }
+
+    /**
+     * Populate the results with collected device info metrics.
+     * @param runMetrics
+     */
+    public void populateDeviceInfoMetrics(Map<String, String> runMetrics) {
+        mDeviceInfo.populateMetrics(runMetrics);
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSummaryXml.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSummaryXml.java
index b91a391..4f0b59b 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSummaryXml.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSummaryXml.java
@@ -103,11 +103,11 @@
                     CtsXmlResultReporter.RESULT_TAG)) {
                 mPlan = getAttribute(parser, CtsXmlResultReporter.PLAN_ATTR);
             } else if (eventType == XmlPullParser.START_TAG && parser.getName().equals(
-                    CtsXmlResultReporter.SUMMARY_TAG)) {
-                mNumFailed = parseIntAttr(parser, CtsXmlResultReporter.FAILED_ATTR) +
-                    parseIntAttr(parser, CtsXmlResultReporter.TIMEOUT_ATTR);
-                mNumNotExecuted = parseIntAttr(parser, CtsXmlResultReporter.NOT_EXECUTED_ATTR);
-                mNumPassed = parseIntAttr(parser, CtsXmlResultReporter.PASS_ATTR);
+                    TestResults.SUMMARY_TAG)) {
+                mNumFailed = parseIntAttr(parser, TestResults.FAILED_ATTR) +
+                    parseIntAttr(parser, TestResults.TIMEOUT_ATTR);
+                mNumNotExecuted = parseIntAttr(parser, TestResults.NOT_EXECUTED_ATTR);
+                mNumPassed = parseIntAttr(parser, TestResults.PASS_ATTR);
                 return;
               }
             eventType = parser.next();
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
index 7566fce..dcf013c 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
@@ -145,7 +145,7 @@
         assertTrue(output.contains(
                 "<Summary failed=\"1\" notExecuted=\"0\" timeout=\"0\" pass=\"0\" />"));
         final String failureTag =
-                "<FailedScene message=\"this is a trace\">      <StackTrace>this is a tracemore trace";
+                "<FailedScene message=\"this is a trace\">     <StackTrace>this is a tracemore trace";
         assertTrue(output.contains(failureTag));
     }
 
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/DeviceInfoResultTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/DeviceInfoResultTest.java
new file mode 100644
index 0000000..a0388b2
--- /dev/null
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/DeviceInfoResultTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.tradefed.result;
+
+import android.tests.getinfo.DeviceInfoConstants;
+
+import junit.framework.TestCase;
+
+import org.kxml2.io.KXmlSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link DeviceInfoResult}
+ */
+public class DeviceInfoResultTest extends TestCase {
+
+    private DeviceInfoResult mDeserializingInfo;
+
+    @Override
+    protected void setUp() throws Exception {
+        mDeserializingInfo = new DeviceInfoResult() {
+            // override parent to advance xml parser to correct tag
+            @Override
+            void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+                int eventType = parser.getEventType();
+                while (eventType != XmlPullParser.END_DOCUMENT) {
+                    if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TAG)) {
+                        super.parse(parser);
+                        return;
+                    }
+                    eventType = parser.next();
+                }
+                throw new XmlPullParserException(String.format("Could not find tag %s", TAG));
+            }
+        };
+    }
+
+    /**
+     * Test the roundtrip setting of device process details, serializing and parsing from/to XML.
+     */
+    public void testProcess() throws Exception {
+        final String processString = "ueventd;netd;";
+        DeviceInfoResult serializedInfo = new DeviceInfoResult();
+        addMetric(DeviceInfoConstants.PROCESSES, processString, serializedInfo);
+        String serializedOutput = serialize(serializedInfo);
+        mDeserializingInfo.parse(new StringReader(serializedOutput));
+        assertEquals(processString, mDeserializingInfo.getMetrics().get(
+                DeviceInfoConstants.PROCESSES));
+    }
+
+    /**
+     * Test the roundtrip setting of device feature details, serializing and parsing from/to XML.
+     */
+    public void testFeature() throws Exception {
+        final String featureString =
+            "android.hardware.audio.low_latency:sdk:false;android.hardware.bluetooth:sdk:true;";
+        DeviceInfoResult serializedInfo = new DeviceInfoResult();
+        addMetric(DeviceInfoConstants.FEATURES, featureString, serializedInfo);
+        String serializedOutput = serialize(serializedInfo);
+        mDeserializingInfo.parse(new StringReader(serializedOutput));
+        assertEquals(featureString, mDeserializingInfo.getMetrics().get(
+                DeviceInfoConstants.FEATURES));
+    }
+
+    /**
+     * Test populating a combined metric like device serial
+     */
+    public void testPopulateMetrics_combinedSerial() throws Exception {
+        DeviceInfoResult info = new DeviceInfoResult();
+        // first add another metric to make hashmap non empty, so combined logic is triggered
+        addMetric(DeviceInfoConstants.PROCESSES, "proc", info);
+        addMetric(DeviceInfoConstants.SERIAL_NUMBER, "device1", info);
+        // ensure the stored serial number equals the value that was just set
+        assertEquals("device1", info.getMetrics().get(
+                DeviceInfoConstants.SERIAL_NUMBER));
+        // now add it again
+        addMetric(DeviceInfoConstants.SERIAL_NUMBER, "device1", info);
+        // should still equal same value
+        assertEquals("device1", info.getMetrics().get(
+                DeviceInfoConstants.SERIAL_NUMBER));
+        // now store different serial, and expect csv
+        addMetric(DeviceInfoConstants.SERIAL_NUMBER, "device2", info);
+        assertEquals("device1,device2", info.getMetrics().get(
+                DeviceInfoConstants.SERIAL_NUMBER));
+    }
+
+    /**
+     * Test populating a verified-to-be-identical metric like DeviceInfoConstants.BUILD_FINGERPRINT
+     */
+    public void testPopulateMetrics_verify() throws Exception {
+        DeviceInfoResult info = new DeviceInfoResult();
+        addMetric(DeviceInfoConstants.BUILD_FINGERPRINT, "fingerprint1", info);
+        // ensure the stored fingerprint equals the value that was just set
+        assertEquals("fingerprint1", info.getMetrics().get(
+                DeviceInfoConstants.BUILD_FINGERPRINT));
+        // now add it again
+        addMetric(DeviceInfoConstants.BUILD_FINGERPRINT, "fingerprint1", info);
+        // should still equal same value
+        assertEquals("fingerprint1", info.getMetrics().get(
+                DeviceInfoConstants.BUILD_FINGERPRINT));
+        // now store different serial, and expect error message
+        addMetric(DeviceInfoConstants.BUILD_FINGERPRINT, "fingerprint2", info);
+        assertTrue(info.getMetrics().get(
+                DeviceInfoConstants.BUILD_FINGERPRINT).contains("ERROR"));
+    }
+
+    /**
+     * Helper method to add given metric to the {@link DeviceInfoResult}
+     */
+    private void addMetric(String metricName, String metricValue, DeviceInfoResult serializedInfo) {
+        Map<String, String> collectedMetrics = new HashMap<String, String>();
+        collectedMetrics.put(metricName, metricValue);
+        serializedInfo.populateMetrics(collectedMetrics);
+    }
+
+    /**
+     * Helper method to serialize given object to XML
+     */
+    private String serialize(DeviceInfoResult serializedInfo)
+            throws IOException {
+        KXmlSerializer xmlSerializer = new KXmlSerializer();
+        StringWriter serializedOutput = new StringWriter();
+        xmlSerializer.setOutput(serializedOutput);
+        serializedInfo.serialize(xmlSerializer);
+        return serializedOutput.toString();
+    }
+}