Support merging device info metrics.

Add support to read in previous device metrics from XML, and add logic to
combine values. ie when running on multiple devices, we want the report to
show: serial1,serial2 etc

Also cleanup a few constants for consistency.

Bug 5171702

Change-Id: I53a1d82b78fde7795f72015c289baa28c7b8472e
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();
+    }
+}