Change CTS Verifier reporting to CTSv2 format.

Bug: 32723464
Test: CTS Verifier should make a report in the CTSv2 format. CTS results
should stay consistent.
Make and run compatibility-common-util-tests. The unit tests should pass.

Change-Id: If3cc9dbc83f7eda356d7dbe36dd28615f492bcfe
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java b/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java
index 33c9b62..64585f2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java
@@ -21,11 +21,21 @@
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Environment;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.IInvocationResult;
+import com.android.compatibility.common.util.InvocationResult;
+import com.android.compatibility.common.util.ResultHandler;
+
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.BufferedOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+// import java.nio.file.Files;
+// import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
@@ -38,6 +48,21 @@
  * Background task to generate a report and save it to external storage.
  */
 class ReportExporter extends AsyncTask<Void, Void, String> {
+
+    private static final String COMMAND_LINE_ARGS = "CtsVerifier";
+    private static final String LOG_URL = null;
+    private static final String REFERENCE_URL = null;
+    private static final String SUITE_NAME = "ctsverifier";
+    private static final String SUITE_PLAN = "CTSVERIFIER";
+    private static final String SUITE_BUILD = "0";
+
+    private static final long START_MS = SystemClock.uptimeMillis();
+    private static final long END_MS = START_MS;
+
+    private static final int BUFFER_SIZE = 16 * 1024;
+    private static final String REPORT_DIRECTORY = "ctsVerifierReports";
+    private static final String ZIP_EXTENSION = ".zip";
+
     protected static final Logger LOG = Logger.getLogger(ReportExporter.class.getName());
 
     private final Context mContext;
@@ -54,50 +79,69 @@
             LOG.log(Level.WARNING, "External storage is not writable.");
             return mContext.getString(R.string.no_storage);
         }
-        byte[] contents;
+        IInvocationResult result;
         try {
             TestResultsReport report = new TestResultsReport(mContext, mAdapter);
-            contents = report.getContents().getBytes();
+            result = report.generateResult();
         } catch (Exception e) {
             LOG.log(Level.WARNING, "Couldn't create test results report", e);
             return mContext.getString(R.string.test_results_error);
         }
-        File reportPath = new File(Environment.getExternalStorageDirectory(), "ctsVerifierReports");
-        reportPath.mkdirs();
+        // create a directory for CTS Verifier reports
+        File externalStorageDirectory = Environment.getExternalStorageDirectory();
+        File verifierReportsDir = new File(externalStorageDirectory, REPORT_DIRECTORY);
+        verifierReportsDir.mkdirs();
+        // create a temporary directory for this particular report
+        File tempDir = new File(verifierReportsDir, getReportName());
+        tempDir.mkdirs();
 
-        String baseName = getReportBaseName();
-        File reportFile = new File(reportPath, baseName + ".zip");
-        ZipOutputStream out = null;
+        // create a File object for a report ZIP file
+        File reportZipFile = new File(verifierReportsDir, getReportName() + ZIP_EXTENSION);
+        // create a File object for the result XML file generated by ResultHandler
+        File tempXmlFile = new File(tempDir, ResultHandler.TEST_RESULT_FILE_NAME);
+
         try {
-            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(reportFile)));
-            ZipEntry entry = new ZipEntry(baseName + ".xml");
-            out.putNextEntry(entry);
-            out.write(contents);
-        } catch (IOException e) {
+            // Serialize the report
+            String versionName = Version.getVersionName(mContext);
+            ResultHandler.writeResults(SUITE_NAME, versionName, SUITE_PLAN, SUITE_BUILD,
+                    result, tempDir, START_MS, END_MS, REFERENCE_URL, LOG_URL,
+                    COMMAND_LINE_ARGS);
+
+            // create a compressed ZIP file
+            FileOutputStream reportFileStream = new FileOutputStream(reportZipFile);
+            ZipOutputStream reportZipStream =
+                    new ZipOutputStream(new BufferedOutputStream(reportFileStream));
+            ZipEntry entry = new ZipEntry(ResultHandler.TEST_RESULT_FILE_NAME);
+            reportZipStream.putNextEntry(entry);
+
+            // TODO: compress all results via tradefed ZipUtil or equivalent;
+            // the current implementation only saves the generated result XML
+
+            // write the report to the ZIP file and close the ZIP file
+            FileInputStream tempXmlStream = new FileInputStream(tempXmlFile);
+            int size = -1;
+            byte[] ioBuffer = new byte[BUFFER_SIZE];
+            while ((size = tempXmlStream.read(ioBuffer)) != -1) {
+                reportZipStream.write(ioBuffer, 0, size);
+            }
+            reportZipStream.close();
+
+        } catch (IOException | XmlPullParserException e) {
             LOG.log(Level.WARNING, "I/O exception writing report to storage.", e);
             return mContext.getString(R.string.no_storage);
         } finally {
-            try {
-                if (out != null) {
-                    out.close();
-                }
-            } catch (IOException e) {
-                LOG.log(Level.WARNING, "I/O exception closing report.", e);
-            }
+            // delete the temporary results file and directory made for the reports
+            tempXmlFile.delete();
+            tempDir.delete();
         }
-
-        return mContext.getString(R.string.report_saved, reportFile.getPath());
+        return mContext.getString(R.string.report_saved, reportZipFile.getPath());
     }
 
-    private String getReportBaseName() {
-        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd-HH.mm.ss", Locale.ENGLISH);
+    private String getReportName() {
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss", Locale.ENGLISH);
         String date = dateFormat.format(new Date());
-        return "ctsVerifierReport"
-                + "-" + date
-                + "-" + Build.MANUFACTURER
-                + "-" + Build.PRODUCT
-                + "-" + Build.DEVICE
-                + "-" + Build.ID;
+        return String.format( "%s-%s-%s-%s-%s",
+                date, Build.MANUFACTURER, Build.PRODUCT, Build.DEVICE, Build.ID);
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
index 1e3f312..4dd7777 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
@@ -34,8 +34,6 @@
 import android.view.Window;
 import android.widget.Toast;
 
-import java.io.IOException;
-
 /** Top-level {@link ListActivity} for launching tests and managing results. */
 public class TestListActivity extends AbstractTestListActivity implements View.OnClickListener {
     private static final int CTS_VERIFIER_PERMISSION_REQUEST = 1;
@@ -146,15 +144,10 @@
     }
 
     private void handleViewItemSelected() {
-        try {
-            TestResultsReport report = new TestResultsReport(this, mAdapter);
-            Intent intent = new Intent(this, ReportViewerActivity.class);
-            intent.putExtra(ReportViewerActivity.EXTRA_REPORT_CONTENTS, report.getContents());
-            startActivity(intent);
-        } catch (IOException e) {
-            Toast.makeText(this, R.string.test_results_error, Toast.LENGTH_SHORT).show();
-            Log.e(TAG, "Couldn't copy test results report", e);
-        }
+        TestResultsReport report = new TestResultsReport(this, mAdapter);
+        Intent intent = new Intent(this, ReportViewerActivity.class);
+        intent.putExtra(ReportViewerActivity.EXTRA_REPORT_CONTENTS, report.getContents());
+        startActivity(intent);
     }
 
     private void handleExportItemSelected() {
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java
index 36be7f9..9d9739d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsReport.java
@@ -21,8 +21,15 @@
 import android.text.TextUtils;
 import android.util.Xml;
 
+import com.android.compatibility.common.util.DevicePropertyInfo;
+import com.android.compatibility.common.util.ICaseResult;
+import com.android.compatibility.common.util.IInvocationResult;
+import com.android.compatibility.common.util.IModuleResult;
+import com.android.compatibility.common.util.InvocationResult;
+import com.android.compatibility.common.util.ITestResult;
 import com.android.compatibility.common.util.MetricsXmlSerializer;
 import com.android.compatibility.common.util.ReportLog;
+import com.android.compatibility.common.util.TestStatus;
 import com.android.cts.verifier.TestListAdapter.TestListItem;
 
 import org.xmlpull.v1.XmlSerializer;
@@ -33,26 +40,10 @@
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
+import java.util.Map.Entry;
 
 /**
- * XML text report of the current test results.
- * <p>
- * Sample:
- * <pre>
- * <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
- * <test-results-report report-version="1" creation-time="Tue Jun 28 11:04:10 PDT 2011">
- *   <verifier-info version-name="2.3_r4" version-code="2" />
- *   <device-info>
- *     <build-info fingerprint="google/soju/crespo:2.3.4/GRJ22/121341:user/release-keys" />
- *   </device-info>
- *   <test-results>
- *     <test title="Audio Quality Verifier" class-name="com.android.cts.verifier.audioquality.AudioQualityVerifierActivity" result="not-executed" />
- *     <test title="Hardware/Software Feature Summary" class-name="com.android.cts.verifier.features.FeatureSummaryActivity" result="fail" />
- *     <test title="Bluetooth Test" class-name="com.android.cts.verifier.bluetooth.BluetoothTestActivity" result="fail" />
- *     <test title="Accelerometer Test" class-name="com.android.cts.verifier.sensors.AccelerometerTestActivity" result="pass" />
- *   </test-results>
- * </test-results-report>
- * </pre>
+ * Helper class for creating an {@code InvocationResult} for CTS result generation.
  */
 class TestResultsReport {
 
@@ -63,6 +54,7 @@
     private static DateFormat DATE_FORMAT =
             new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH);
 
+    private static final String PREFIX_TAG = "build_";
     private static final String TEST_RESULTS_REPORT_TAG = "test-results-report";
     private static final String VERIFIER_INFO_TAG = "verifier-info";
     private static final String DEVICE_INFO_TAG = "device-info";
@@ -71,6 +63,9 @@
     private static final String TEST_TAG = "test";
     private static final String TEST_DETAILS_TAG = "details";
 
+    private static final String MODULE_ID = "noabi CtsVerifier";
+    private static final String TEST_CASE_NAME = "manualTests";
+
     private final Context mContext;
 
     private final TestListAdapter mAdapter;
@@ -80,83 +75,83 @@
         this.mAdapter = adapter;
     }
 
-    String getContents() throws IllegalArgumentException, IllegalStateException, IOException {
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    IInvocationResult generateResult() {
+        String abis = null;
+        String abis32 = null;
+        String abis64 = null;
+        String versionBaseOs = null;
+        String versionSecurityPatch = null;
+        IInvocationResult result = new InvocationResult();
+        IModuleResult moduleResult = result.getOrCreateModule(MODULE_ID);
 
-        XmlSerializer xml = Xml.newSerializer();
-        xml.setOutput(outputStream, "utf-8");
-        xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
-        xml.startDocument("utf-8", true);
+        // Collect build fields available in API level 21
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            abis = TextUtils.join(",", Build.SUPPORTED_ABIS);
+            abis32 = TextUtils.join(",", Build.SUPPORTED_32_BIT_ABIS);
+            abis64 = TextUtils.join(",", Build.SUPPORTED_64_BIT_ABIS);
+        }
 
-        xml.startTag(null, TEST_RESULTS_REPORT_TAG);
-        xml.attribute(null, "report-version", Integer.toString(REPORT_VERSION));
-        xml.attribute(null, "creation-time", DATE_FORMAT.format(new Date()));
+        // Collect build fields available in API level 23
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            versionBaseOs = Build.VERSION.BASE_OS;
+            versionSecurityPatch = Build.VERSION.SECURITY_PATCH;
+        }
 
-        xml.startTag(null, VERIFIER_INFO_TAG);
-        xml.attribute(null, "version-name", Version.getVersionName(mContext));
-        xml.attribute(null, "version-code", Integer.toString(Version.getVersionCode(mContext)));
-        xml.endTag(null, VERIFIER_INFO_TAG);
+        // at the time of writing, the build class has no REFERENCE_FINGERPRINT property
+        String referenceFingerprint = null;
 
-        xml.startTag(null, DEVICE_INFO_TAG);
-        xml.startTag(null, BUILD_INFO_TAG);
-        xml.attribute(null, "board", Build.BOARD);
-        xml.attribute(null, "brand", Build.BRAND);
-        xml.attribute(null, "device", Build.DEVICE);
-        xml.attribute(null, "display", Build.DISPLAY);
-        xml.attribute(null, "fingerprint", Build.FINGERPRINT);
-        xml.attribute(null, "id", Build.ID);
-        xml.attribute(null, "model", Build.MODEL);
-        xml.attribute(null, "product", Build.PRODUCT);
-        xml.attribute(null, "release", Build.VERSION.RELEASE);
-        xml.attribute(null, "sdk", Integer.toString(Build.VERSION.SDK_INT));
-        xml.endTag(null, BUILD_INFO_TAG);
-        xml.endTag(null, DEVICE_INFO_TAG);
+        DevicePropertyInfo devicePropertyInfo = new DevicePropertyInfo(Build.CPU_ABI,
+                Build.CPU_ABI2, abis, abis32, abis64, Build.BOARD, Build.BRAND, Build.DEVICE,
+                Build.FINGERPRINT, Build.ID, Build.MANUFACTURER, Build.MODEL, Build.PRODUCT,
+                referenceFingerprint, Build.SERIAL, Build.TAGS, Build.TYPE, versionBaseOs,
+                Build.VERSION.RELEASE, Integer.toString(Build.VERSION.SDK_INT),
+                versionSecurityPatch);
 
-        xml.startTag(null, TEST_RESULTS_TAG);
+        // add device properties to the result with a prefix tag for each key
+        for (Entry<String, String> entry :
+                devicePropertyInfo.getPropertytMapWithPrefix(PREFIX_TAG).entrySet()) {
+            String entryValue = entry.getValue();
+            if (entryValue != null) {
+                result.addInvocationInfo(entry.getKey(), entry.getValue());
+            }
+        }
+
+        ICaseResult caseResult = moduleResult.getOrCreateResult(TEST_CASE_NAME);
         int count = mAdapter.getCount();
         for (int i = 0; i < count; i++) {
             TestListItem item = mAdapter.getItem(i);
             if (item.isTest()) {
-                xml.startTag(null, TEST_TAG);
-                xml.attribute(null, "title", item.title);
-                xml.attribute(null, "class-name", item.testName);
-                xml.attribute(null, "result", getTestResultString(mAdapter.getTestResult(i)));
+                ITestResult currentTestResult = caseResult.getOrCreateResult(item.testName);
+                currentTestResult.setResultStatus(getTestResultStatus(mAdapter.getTestResult(i)));
+                // TODO: report test details with Extended Device Info (EDI) or CTS metrics
+                // String details = mAdapter.getTestDetails(i);
 
-                String details = mAdapter.getTestDetails(i);
-                if (!TextUtils.isEmpty(details)) {
-                    xml.startTag(null, TEST_DETAILS_TAG);
-                    xml.text(details);
-                    xml.endTag(null, TEST_DETAILS_TAG);
-                }
-
-                // TODO(stuartscott): For v2: ReportLog.serialize(xml, mAdapter.getReportLog(i));
                 ReportLog reportLog = mAdapter.getReportLog(i);
                 if (reportLog != null) {
-                    MetricsXmlSerializer metricsXmlSerializer = new MetricsXmlSerializer(xml);
-                    metricsXmlSerializer.serialize(reportLog);
+                    currentTestResult.setReportLog(reportLog);
                 }
-
-                xml.endTag(null, TEST_TAG);
             }
         }
-        xml.endTag(null, TEST_RESULTS_TAG);
+        moduleResult.setDone(true);
 
-        xml.endTag(null, TEST_RESULTS_REPORT_TAG);
-        xml.endDocument();
-
-        return outputStream.toString("utf-8");
+        return result;
     }
 
-    private String getTestResultString(int testResult) {
+    String getContents() {
+        // TODO: remove getContents and everything that depends on it
+        return "Report viewing is deprecated. See contents on the SD Card.";
+    }
+
+    private TestStatus getTestResultStatus(int testResult) {
         switch (testResult) {
             case TestResult.TEST_RESULT_PASSED:
-                return "pass";
+                return TestStatus.PASS;
 
             case TestResult.TEST_RESULT_FAILED:
-                return "fail";
+                return TestStatus.FAIL;
 
             case TestResult.TEST_RESULT_NOT_EXECUTED:
-                return "not-executed";
+                return null;
 
             default:
                 throw new IllegalArgumentException("Unknown test result: " + testResult);
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java
index cd1c911..0805b31 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/targetprep/DeviceInfoCollector.java
@@ -19,6 +19,7 @@
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.compatibility.common.tradefed.testtype.CompatibilityTest;
 import com.android.compatibility.common.tradefed.util.CollectorUtil;
+import com.android.compatibility.common.util.DevicePropertyInfo;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.device.DeviceNotAvailableException;
@@ -39,30 +40,29 @@
  */
 public class DeviceInfoCollector extends ApkInstrumentationPreparer {
 
-    private static final Map<String, String> BUILD_KEYS = new HashMap<>();
-    static {
-        BUILD_KEYS.put("cts:build_id", "ro.build.id");
-        BUILD_KEYS.put("cts:build_product", "ro.product.name");
-        BUILD_KEYS.put("cts:build_device", "ro.product.device");
-        BUILD_KEYS.put("cts:build_board", "ro.product.board");
-        BUILD_KEYS.put("cts:build_manufacturer", "ro.product.manufacturer");
-        BUILD_KEYS.put("cts:build_brand", "ro.product.brand");
-        BUILD_KEYS.put("cts:build_model", "ro.product.model");
-        BUILD_KEYS.put("cts:build_type", "ro.build.type");
-        BUILD_KEYS.put("cts:build_tags", "ro.build.tags");
-        BUILD_KEYS.put("cts:build_fingerprint", "ro.build.fingerprint");
-        BUILD_KEYS.put("cts:build_abi", "ro.product.cpu.abi");
-        BUILD_KEYS.put("cts:build_abi2", "ro.product.cpu.abi2");
-        BUILD_KEYS.put("cts:build_abis", "ro.product.cpu.abilist");
-        BUILD_KEYS.put("cts:build_abis_32", "ro.product.cpu.abilist32");
-        BUILD_KEYS.put("cts:build_abis_64", "ro.product.cpu.abilist64");
-        BUILD_KEYS.put("cts:build_serial", "ro.serialno");
-        BUILD_KEYS.put("cts:build_version_release", "ro.build.version.release");
-        BUILD_KEYS.put("cts:build_version_sdk", "ro.build.version.sdk");
-        BUILD_KEYS.put("cts:build_version_base_os", "ro.build.version.base_os");
-        BUILD_KEYS.put("cts:build_version_security_patch", "ro.build.version.security_patch");
-        BUILD_KEYS.put("cts:build_reference_fingerprint", "ro.build.reference.fingerprint");
-    }
+    private static final String ABI = "ro.product.cpu.abi";
+    private static final String ABI2 = "ro.product.cpu.abi2";
+    private static final String ABIS = "ro.product.cpu.abilist";
+    private static final String ABIS_32 = "ro.product.cpu.abilist32";
+    private static final String ABIS_64 = "ro.product.cpu.abilist64";
+    private static final String BOARD = "ro.product.board";
+    private static final String BRAND = "ro.product.brand";
+    private static final String DEVICE = "ro.product.device";
+    private static final String FINGERPRINT = "ro.build.fingerprint";
+    private static final String ID = "ro.build.id";
+    private static final String MANUFACTURER = "ro.product.manufacturer";
+    private static final String MODEL = "ro.product.model";
+    private static final String PRODUCT = "ro.product.name";
+    private static final String REFERENCE_FINGERPRINT = "ro.build.reference.fingerprint";
+    private static final String SERIAL = "ro.serialno";
+    private static final String TAGS = "ro.build.tags";
+    private static final String TYPE = "ro.build.type";
+    private static final String VERSION_BASE_OS = "ro.build.version.base_os";
+    private static final String VERSION_RELEASE = "ro.build.version.release";
+    private static final String VERSION_SDK = "ro.build.version.sdk";
+    private static final String VERSION_SECURITY_PATCH = "ro.build.version.security_patch";
+
+    private static final String PREFIX_TAG = "cts:build_";
 
     @Option(name = CompatibilityTest.SKIP_DEVICE_INFO_OPTION,
             shortName = 'd',
@@ -91,9 +91,16 @@
     @Override
     public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
             BuildError, DeviceNotAvailableException {
-        for (Entry<String, String> entry : BUILD_KEYS.entrySet()) {
-            buildInfo.addBuildAttribute(
-                    entry.getKey(), nullToEmpty(device.getProperty(entry.getValue())));
+        DevicePropertyInfo devicePropertyInfo = new DevicePropertyInfo(ABI, ABI2, ABIS, ABIS_32,
+                ABIS_64, BOARD, BRAND, DEVICE, FINGERPRINT, ID, MANUFACTURER, MODEL, PRODUCT,
+                REFERENCE_FINGERPRINT, SERIAL, TAGS, TYPE, VERSION_BASE_OS, VERSION_RELEASE,
+                VERSION_SDK, VERSION_SECURITY_PATCH);
+
+        // add device properties to the result with a prefix tag for each key
+        for (Entry<String, String> entry :
+                devicePropertyInfo.getPropertytMapWithPrefix(PREFIX_TAG).entrySet()) {
+            buildInfo.addBuildAttribute(entry.getKey(),
+                    nullToEmpty(device.getProperty(entry.getValue())));
         }
         if (mSkipDeviceInfo) {
             return;
@@ -111,7 +118,7 @@
             return;
         }
         if (mHostDir != null && mHostDir.isDirectory() &&
-                    mResultDir != null && mResultDir.isDirectory()) {
+                mResultDir != null && mResultDir.isDirectory()) {
             CollectorUtil.pullFromHost(mHostDir, mResultDir);
         }
     }
diff --git a/common/util/Android.mk b/common/util/Android.mk
index c95508b..0d3754b 100644
--- a/common/util/Android.mk
+++ b/common/util/Android.mk
@@ -26,6 +26,8 @@
 
 LOCAL_MODULE := compatibility-common-util-devicesidelib
 
+LOCAL_STATIC_JAVA_LIBRARIES := guava
+
 LOCAL_SDK_VERSION := current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
@@ -42,7 +44,10 @@
 
 LOCAL_MODULE := compatibility-common-util-hostsidelib
 
-LOCAL_STATIC_JAVA_LIBRARIES := junit kxml2-2.3.0 platform-test-annotations-host
+LOCAL_STATIC_JAVA_LIBRARIES :=  guavalib \
+                                junit \
+                                kxml2-2.3.0 \
+                                platform-test-annotations-host
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/ChecksumReporter.java b/common/util/src/com/android/compatibility/common/util/ChecksumReporter.java
similarity index 99%
rename from common/host-side/util/src/com/android/compatibility/common/util/ChecksumReporter.java
rename to common/util/src/com/android/compatibility/common/util/ChecksumReporter.java
index faac61f..32fa532 100644
--- a/common/host-side/util/src/com/android/compatibility/common/util/ChecksumReporter.java
+++ b/common/util/src/com/android/compatibility/common/util/ChecksumReporter.java
@@ -16,8 +16,6 @@
 
 package com.android.compatibility.common.util;
 
-import com.android.annotations.Nullable;
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
@@ -316,7 +314,7 @@
     }
 
     private static String buildTestId(
-            String suiteName, String caseName, String testName, @Nullable String abi) {
+            String suiteName, String caseName, String testName, String abi) {
         String name = Joiner.on(NAME_SEPARATOR).skipNulls().join(
                 Strings.emptyToNull(suiteName),
                 Strings.emptyToNull(caseName),
diff --git a/common/util/src/com/android/compatibility/common/util/DevicePropertyInfo.java b/common/util/src/com/android/compatibility/common/util/DevicePropertyInfo.java
new file mode 100644
index 0000000..ec24b42
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/DevicePropertyInfo.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 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.compatibility.common.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class for collecting device information. This is used to enforce
+ * consistent property collection host-side and device-side for CTS reports.
+ *
+ * Note that properties across sources can differ, e.g. {@code android.os.Build}
+ * properties sometimes deviate from the read-only properties that they're based
+ * on.
+ */
+public final class DevicePropertyInfo {
+
+    private final String mAbi;
+    private final String mAbi2;
+    private final String mAbis;
+    private final String mAbis32;
+    private final String mAbis64;
+    private final String mBoard;
+    private final String mBrand;
+    private final String mDevice;
+    private final String mFingerprint;
+    private final String mId;
+    private final String mManufacturer;
+    private final String mModel;
+    private final String mProduct;
+    private final String mReferenceFingerprint;
+    private final String mSerial;
+    private final String mTags;
+    private final String mType;
+    private final String mVersionBaseOs;
+    private final String mVersionRelease;
+    private final String mVersionSdk;
+    private final String mVersionSecurityPatch;
+
+    public DevicePropertyInfo(String abi, String abi2, String abis, String abis32, String abis64,
+            String board, String brand, String device, String fingerprint, String id,
+            String manufacturer, String model, String product, String referenceFigerprint,
+            String serial, String tags, String type, String versionBaseOs, String versionRelease,
+            String versionSdk, String versionSecurityPatch) {
+        mAbi = abi;
+        mAbi2 = abi2;
+        mAbis = abis;
+        mAbis32 = abis32;
+        mAbis64 = abis64;
+        mBoard = board;
+        mBrand = brand;
+        mDevice = device;
+        mFingerprint = fingerprint;
+        mId = id;
+        mManufacturer = manufacturer;
+        mModel = model;
+        mProduct = product;
+        mReferenceFingerprint = referenceFigerprint;
+        mSerial = serial;
+        mTags = tags;
+        mType = type;
+        mVersionBaseOs = versionBaseOs;
+        mVersionRelease = versionRelease;
+        mVersionSdk = versionSdk;
+        mVersionSecurityPatch = versionSecurityPatch;
+    }
+
+    /**
+     * Return a {@code Map} with property keys prepended with a given prefix
+     * string. This is intended to be used to generate entries for
+     * {@code} Build tag attributes in CTS test results.
+     */
+    public Map<String, String> getPropertytMapWithPrefix(String prefix) {
+        Map<String, String> propertyMap = new HashMap<>();
+
+        propertyMap.put(prefix + "abi", mAbi);
+        propertyMap.put(prefix + "abi2", mAbi2);
+        propertyMap.put(prefix + "abis", mAbis);
+        propertyMap.put(prefix + "abis_32", mAbis32);
+        propertyMap.put(prefix + "abis_64", mAbis64);
+        propertyMap.put(prefix + "board", mBoard);
+        propertyMap.put(prefix + "brand", mBrand);
+        propertyMap.put(prefix + "device", mDevice);
+        propertyMap.put(prefix + "fingerprint", mFingerprint);
+        propertyMap.put(prefix + "id", mId);
+        propertyMap.put(prefix + "manufacturer", mManufacturer);
+        propertyMap.put(prefix + "model", mModel);
+        propertyMap.put(prefix + "product", mProduct);
+        propertyMap.put(prefix + "reference_fingerprint", mReferenceFingerprint);
+        propertyMap.put(prefix + "serial", mSerial);
+        propertyMap.put(prefix + "tags", mTags);
+        propertyMap.put(prefix + "type", mType);
+        propertyMap.put(prefix + "version_base_os", mVersionBaseOs);
+        propertyMap.put(prefix + "version_release", mVersionRelease);
+        propertyMap.put(prefix + "version_sdk", mVersionSdk);
+        propertyMap.put(prefix + "version_security_patch", mVersionSecurityPatch);
+
+        return propertyMap;
+    }
+
+}
diff --git a/common/util/src/com/android/compatibility/common/util/ModuleResult.java b/common/util/src/com/android/compatibility/common/util/ModuleResult.java
index 60038cf..16d8964 100644
--- a/common/util/src/com/android/compatibility/common/util/ModuleResult.java
+++ b/common/util/src/com/android/compatibility/common/util/ModuleResult.java
@@ -168,7 +168,8 @@
      */
     @Override
     public String getName() {
-        return AbiUtils.parseTestName(mId);
+        // TODO: switch to using AbiUtils#parseTestName when available
+        return parseId(mId)[1];
     }
 
     /**
@@ -176,7 +177,21 @@
      */
     @Override
     public String getAbi() {
-        return AbiUtils.parseAbi(mId);
+        // TODO: switch to using AbiUtils#parseAbi when available
+        return parseId(mId)[0];
+    }
+
+    /**
+     * Parses a unique id into the ABI and name.
+     * @param id The id to parse.
+     * @return a string array containing the ABI and name.
+     */
+    private static String[] parseId(String id) {
+        // TODO: remove this when AbiUtils is available for getName and getAbi
+        if (id == null || !id.contains(" ")) {
+            return new String[] {"", ""};
+        }
+        return id.split(" ");
     }
 
     /**
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/ResultHandler.java b/common/util/src/com/android/compatibility/common/util/ResultHandler.java
similarity index 96%
rename from common/host-side/util/src/com/android/compatibility/common/util/ResultHandler.java
rename to common/util/src/com/android/compatibility/common/util/ResultHandler.java
index 89ec2d4..1cdc38a 100644
--- a/common/host-side/util/src/com/android/compatibility/common/util/ResultHandler.java
+++ b/common/util/src/com/android/compatibility/common/util/ResultHandler.java
@@ -25,6 +25,7 @@
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.FileReader;
@@ -33,9 +34,6 @@
 import java.io.OutputStream;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -60,7 +58,7 @@
     private static final String TYPE = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer";
     private static final String NS = null;
     private static final String RESULT_FILE_VERSION = "5.0";
-    /* package */ static final String TEST_RESULT_FILE_NAME = "test_result.xml";
+    public static final String TEST_RESULT_FILE_NAME = "test_result.xml";
     private static final String FAILURE_REPORT_NAME = "test_result_failures.html";
     private static final String FAILURE_XSL_FILE_NAME = "compatibility_failures.xsl";
 
@@ -182,7 +180,8 @@
                     parser.require(XmlPullParser.START_TAG, NS, MODULE_TAG);
                     String name = parser.getAttributeValue(NS, NAME_ATTR);
                     String abi = parser.getAttributeValue(NS, ABI_ATTR);
-                    String moduleId = AbiUtils.createId(abi, name);
+                    // TODO: use AbiUtils#createId when available for use
+                    String moduleId = String.format("%s %s", abi, name);
                     boolean done = Boolean.parseBoolean(parser.getAttributeValue(NS, DONE_ATTR));
                     IModuleResult module = invocation.getOrCreateModule(moduleId);
                     module.initializeDone(done);
@@ -451,18 +450,22 @@
                 // If the previous run has an invalid checksum file,
                 // copy it into current results folder for future troubleshooting
                 File retryDirectory = invocationResult.getRetryDirectory();
-                Path retryChecksum = FileSystems.getDefault().getPath(
-                        retryDirectory.getAbsolutePath(), ChecksumReporter.NAME);
-                if (!retryChecksum.toFile().exists()) {
+                File retryChecksum = new File(retryDirectory, ChecksumReporter.NAME);
+                if (!retryChecksum.exists()) {
                     // if no checksum file, check for a copy from a previous retry
-                    retryChecksum = FileSystems.getDefault().getPath(
-                            retryDirectory.getAbsolutePath(), ChecksumReporter.PREV_NAME);
+                    retryChecksum = new File(retryDirectory, ChecksumReporter.PREV_NAME);
                 }
 
-                if (retryChecksum.toFile().exists()) {
+                if (retryChecksum.exists()) {
                     File checksumCopy = new File(resultDir, ChecksumReporter.PREV_NAME);
-                    try (FileOutputStream stream = new FileOutputStream(checksumCopy)) {
-                        Files.copy(retryChecksum, stream);
+                    try (OutputStream out = new FileOutputStream(checksumCopy);
+                        InputStream in = new FileInputStream(retryChecksum)) {
+                        // Copy the bits from input stream to output stream
+                        byte[] buf = new byte[1024];
+                        int len;
+                        while ((len = in.read(buf)) > 0) {
+                            out.write(buf, 0, len);
+                        }
                     } catch (IOException e) {
                         // Do not disrupt the process if there is a problem copying checksum
                     }
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java b/common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
similarity index 97%
rename from common/host-side/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
rename to common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
index 0dfe3f3..4f89a78 100644
--- a/common/host-side/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
+++ b/common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
@@ -70,13 +70,8 @@
     private static final String METHOD_3 = "testBlah3";
     private static final String METHOD_4 = "testBlah4";
     private static final String SUMMARY_SOURCE = String.format("%s#%s:20", CLASS_B, METHOD_4);
-    private static final String DETAILS_SOURCE = String.format("%s#%s:18", CLASS_B, METHOD_4);
     private static final String SUMMARY_MESSAGE = "Headline";
     private static final double SUMMARY_VALUE = 9001;
-    private static final String DETAILS_MESSAGE = "Deats";
-    private static final double DETAILS_VALUE_1 = 14;
-    private static final double DETAILS_VALUE_2 = 18;
-    private static final double DETAILS_VALUE_3 = 17;
     private static final String MESSAGE = "Something small is not alright";
     private static final String STACK_TRACE = "Something small is not alright\n " +
             "at four.big.insects.Marley.sing(Marley.java:10)";
diff --git a/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java b/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
index e6c6a87..faa4690 100644
--- a/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
+++ b/common/util/tests/src/com/android/compatibility/common/util/UnitTests.java
@@ -34,6 +34,7 @@
         addTestSuite(ModuleResultTest.class);
         addTestSuite(MultipartFormTest.class);
         addTestSuite(ReportLogTest.class);
+        addTestSuite(ResultHandlerTest.class);
         addTestSuite(StatTest.class);
         addTestSuite(TestFilterTest.class);
         addTestSuite(TestResultTest.class);
diff --git a/tests/tests/content/Android.mk b/tests/tests/content/Android.mk
index a22d539..d901926 100644
--- a/tests/tests/content/Android.mk
+++ b/tests/tests/content/Android.mk
@@ -23,7 +23,15 @@
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 ctsdeviceutil ctstestrunner services.core
+LOCAL_STATIC_JAVA_LIBRARIES :=  android-support-v4 \
+                                android-support-multidex \
+                                ctsdeviceutil \
+                                ctstestrunner \
+                                services.core
+
+# Use multi-dex as the compatibility-common-util-devicesidelib dependency
+# on ctsdeviceutil pushes us beyond 64k methods.
+LOCAL_JACK_FLAGS := --multi-dex legacy
 
 # Resource unit tests use a private locale and some densities
 LOCAL_AAPT_FLAGS = -c xx_YY -c cs -c small -c normal -c large -c xlarge \