Merge "Wait for destoroying activities on EglConfigTest#testEglConfigs" into nougat-cts-dev
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 0c1a884..9c57f6f 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -71,6 +71,8 @@
             android:debuggable="true"
             android:largeHeap="true">
 
+        <meta-data android:name="SuiteName" android:value="CTS_VERIFIER" />
+
         <meta-data android:name="com.google.android.backup.api_key"
                 android:value="AEdPqrEAAAAIbK6ldcOzoeRtQ1u1dFVJ1A7KetRhit-a1Xa82Q" />
 
@@ -145,14 +147,15 @@
                     android:value="android.software.backup" />
         </activity>
 
-        <activity android:name=".backup.BackupAccessibilityTestActivity" android:label="@string/backup_accessibility_test">
+	<!-- Further work is required for this test, b/32798562  -->
+        <!-- activity android:name=".backup.BackupAccessibilityTestActivity" android:label="@string/backup_accessibility_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.cts.intent.category.MANUAL_TEST" />
             </intent-filter>
             <meta-data android:name="test_required_features"
                     android:value="android.software.backup" />
-        </activity>
+        </activity -->
 
         <activity android:name=".bluetooth.BluetoothTestActivity"
                 android:label="@string/bluetooth_test"
@@ -2091,6 +2094,7 @@
             </intent-filter>
             <meta-data android:name="test_category" android:value="@string/test_category_audio" />
             <meta-data android:name="test_required_features" android:value="android.hardware.microphone" />
+             <meta-data android:name="test_required_features" android:value="android.hardware.usb.host" />
         </activity>
 
         <service android:name=".tv.MockTvInputService"
diff --git a/apps/CtsVerifier/assets/report/compatibility_result.css b/apps/CtsVerifier/assets/report/compatibility_result.css
new file mode 100644
index 0000000..699f45a
--- /dev/null
+++ b/apps/CtsVerifier/assets/report/compatibility_result.css
@@ -0,0 +1,164 @@
+/* Copyright (C) 2015 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.
+*/
+
+body {
+    font-family:arial,sans-serif;
+    color:#000;
+    font-size:13px;
+    color:#333;
+    padding:10;
+    margin:10;
+}
+
+/* Report logo and device name */
+table.title {
+    padding:5px;
+    border-width: 0px;
+    margin-left:auto;
+    margin-right:auto;
+    vertical-align:middle;
+}
+
+table.summary {
+    background-color: rgb(212, 233, 169);
+    border-collapse:collapse;
+    border: 0px solid #A5C639;
+    margin-left:auto;
+    margin-right:auto;
+}
+
+table.summary th {
+    background-color: #A5C639;
+    font-size: 1.2em;
+    padding: 0.5em;
+}
+
+table.summary td {
+    border-width: 0px 0px 0px 0px;
+    border-color: gray;
+    border-style: inset;
+    font-size: 1em;
+    padding: 0.5em;
+    vertical-align: top;
+}
+
+table.testsummary {
+    background-color: rgb(212, 233, 169);
+    border-collapse:collapse;
+    margin-left:auto;
+    margin-right:auto;
+}
+
+table.testsummary th {
+    background-color: #A5C639;
+    border: 1px outset gray;
+    padding: 0.5em;
+}
+
+table.testsummary td {
+    border: 1px outset #A5C639;
+    padding: 0.5em;
+    text-align: center;
+}
+
+table.testdetails {
+    background-color: rgb(212, 233, 169);
+    border-collapse:collapse;
+    border-width:1;
+    border-color: #A5C639;
+    margin-left:auto;
+    margin-right:auto;
+    margin-bottom: 2em;
+    vertical-align: top;
+    width: 95%;
+}
+
+table.testdetails th {
+    background-color: #A5C639;
+    border-width: 1px;
+    border-color: gray;
+    border-style: outset;
+    height: 2em;
+    padding: 0.2em;
+}
+
+table.testdetails td {
+    border-width: 1px;
+    border-color: #A5C639;
+    border-style: outset;
+    text-align: left;
+    vertical-align: top;
+    padding: 0.2em;
+}
+
+table.testdetails td.module {
+    background-color: white;
+    border: 0px;
+    font-weight: bold;
+}
+
+/* Test cell details */
+td.failed {
+    background-color: #FA5858;
+    font-weight:bold;
+    vertical-align: top;
+    text-align: center;
+}
+
+td.failuredetails {
+    text-align: left;
+}
+
+td.pass {
+    text-align: center;
+    margin-left:auto;
+    margin-right:auto;
+}
+
+td.not_executed {
+    background-color: #A5C639;
+    vertical-align: top;
+    text-align: center;
+}
+
+td.testname {
+    border-width: 1px;
+    border-color: #A5C639;
+    border-style: outset;
+    text-align: left;
+    vertical-align: top;
+    padding:1;
+    overflow:hidden;
+}
+
+td.testcase {
+    border-width: 1px;
+    border-color: #A5C639;
+    border-style: outset;
+    text-align: left;
+    vertical-align: top;
+    padding:1;
+    overflow:hidden;
+    font-weight:bold;
+}
+
+div.details {
+    white-space: pre-wrap;       /* css-3 */
+    white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
+    white-space: -pre-wrap;      /* Opera 4-6 */
+    white-space: -o-pre-wrap;    /* Opera 7 */
+    word-wrap: break-word;       /* Internet Explorer 5.5+ */
+    overflow:auto;
+}
diff --git a/apps/CtsVerifier/assets/report/compatibility_result.xsd b/apps/CtsVerifier/assets/report/compatibility_result.xsd
new file mode 100644
index 0000000..9b2758c
--- /dev/null
+++ b/apps/CtsVerifier/assets/report/compatibility_result.xsd
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Copyright (C) 2015 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.
+ -->
+
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           targetNamespace="http://compatibility.android.com/compatibility_result/1.15"
+           xmlns="http://compatibility.android.com/compatibility_result/1.15"
+           elementFormDefault="qualified">
+
+  <xs:element name="Result">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element name="Summary" type="summaryType"/>
+        <xs:element name="Module" type="moduleType" minOccurs="1" maxOccurs="unbounded"/>
+      </xs:sequence>
+      <xs:attribute name="start" type="xs:string"/>
+      <xs:attribute name="end" type="xs:string"/>
+      <xs:attribute name="plan" type="xs:string"/>
+      <xs:attribute name="suite_name" type="xs:string"/>
+      <xs:attribute name="suite_version" type="xs:string"/>
+    </xs:complexType>
+  </xs:element>
+
+  <xs:complexType name="summaryType">
+    <xs:attribute name="failed" type="xs:integer"/>
+    <xs:attribute name="not_executed" type="xs:integer"/>
+    <xs:attribute name="pass" type="xs:integer"/>
+  </xs:complexType>
+
+  <xs:complexType name="moduleType">
+    <xs:sequence>
+      <xs:element name="Test" type="testType" minOccurs="1" maxOccurs="unbounded" />
+    </xs:sequence>
+    <xs:attribute name="name" type="xs:string" use="required"/>
+    <xs:attribute name="abi" type="xs:string"/>
+  </xs:complexType>
+
+  <xs:simpleType name="unitType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="none"/>
+      <xs:enumeration value="ms"/>
+      <xs:enumeration value="fps"/>
+      <xs:enumeration value="ops"/>
+      <xs:enumeration value="kbps"/>
+      <xs:enumeration value="mbps"/>
+      <xs:enumeration value="byte"/>
+      <xs:enumeration value="count"/>
+      <xs:enumeration value="score"/>
+    </xs:restriction>
+  </xs:simpleType>
+
+  <xs:simpleType name="scoreTypeType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="higher-better"/>
+      <xs:enumeration value="lower-better"/>
+      <xs:enumeration value="neutral"/>
+      <xs:enumeration value="warning"/>
+    </xs:restriction>
+  </xs:simpleType>
+
+  <xs:complexType name="testType">
+    <xs:sequence>
+      <xs:element name="Failure" minOccurs="0" maxOccurs="1">
+        <xs:complexType>
+          <xs:sequence>
+            <xs:element name="StackTrace" type="xs:string" minOccurs="0" maxOccurs="1"/>
+          </xs:sequence>
+          <xs:attribute name="message" type="xs:string"/>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="Summary" minOccurs="0" maxOccurs="1">
+        <xs:complexType>
+          <xs:simpleContent>
+            <xs:extension base="xs:decimal">
+              <xs:attribute name="message" type="xs:string" use="required" />
+              <xs:attribute name="scoreType" type="scoreTypeType" use="required" />
+              <xs:attribute name="unit" type="unitType" use="required" />
+              <xs:attribute name="target" type="xs:decimal" />
+            </xs:extension>
+          </xs:simpleContent>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="Details" minOccurs="0" maxOccurs="1">
+        <xs:complexType>
+          <xs:sequence>
+            <xs:element name="ValueArray" minOccurs="0" maxOccurs="unbounded">
+              <xs:complexType>
+                <xs:sequence>
+                  <xs:element name="Value" type="xs:decimal" minOccurs="0" maxOccurs="unbounded" />
+                </xs:sequence>
+                <xs:attribute name="source" type="xs:string" use="required" />
+                <xs:attribute name="message" type="xs:string" use="required" />
+                <xs:attribute name="scoreType" type="scoreTypeType" use="required" />
+                <xs:attribute name="unit" type="unitType" use="required" />
+              </xs:complexType>
+            </xs:element>
+          </xs:sequence>
+        </xs:complexType>
+      </xs:element>
+    </xs:sequence>
+    <xs:attribute name="name" type="xs:string" use="required"/>
+    <xs:attribute name="result" type="resultType" use="required"/>
+    <xs:attribute name="start" type="xs:string"/>
+    <xs:attribute name="end" type="xs:string"/>
+  </xs:complexType>
+
+  <xs:simpleType name="resultType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="pass"/>
+      <xs:enumeration value="fail"/>
+      <xs:enumeration value="not_executed"/>
+    </xs:restriction>
+  </xs:simpleType>
+</xs:schema>
\ No newline at end of file
diff --git a/apps/CtsVerifier/assets/report/compatibility_result.xsl b/apps/CtsVerifier/assets/report/compatibility_result.xsl
new file mode 100644
index 0000000..b8c1245
--- /dev/null
+++ b/apps/CtsVerifier/assets/report/compatibility_result.xsl
@@ -0,0 +1,294 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp "&#160;"> ]>
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+    <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
+
+    <xsl:template match="/">
+
+        <html>
+            <head>
+                <title>Test Report</title>
+                <style type="text/css">
+                    @import "compatibility_result.css";
+                </style>
+            </head>
+            <body>
+                <div>
+                    <table class="title">
+                        <tr>
+                            <td align="left"><img src="logo.png"/></td>
+                        </tr>
+                    </table>
+                </div>
+
+                <div>
+                    <table class="summary">
+                        <tr>
+                            <th colspan="2">Summary</th>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Suite / Plan</td>
+                            <td>
+                                <xsl:value-of select="Result/@suite_name"/> / <xsl:value-of select="Result/@suite_plan"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Suite / Build</td>
+                            <td>
+                                <xsl:value-of select="Result/@suite_version"/> / <xsl:value-of select="Result/@suite_build_number"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Host Info</td>
+                            <td>
+                                Result/@start
+                                <xsl:value-of select="Result/@host_name"/>
+                                (<xsl:value-of select="Result/@os_name"/> - <xsl:value-of select="Result/@os_version"/>)
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Start time / End Time</td>
+                            <td>
+                                <xsl:value-of select="Result/@start_display"/> /
+                                <xsl:value-of select="Result/@end_display"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Tests Passed</td>
+                            <td>
+                                <xsl:value-of select="Result/Summary/@pass"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Tests Failed</td>
+                            <td>
+                                <xsl:value-of select="Result/Summary/@failed"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Tests Not Executed</td>
+                            <td>
+                                <xsl:value-of select="Result/Summary/@not_executed"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Modules Done</td>
+                            <td>
+                                <xsl:value-of select="Result/Summary/@modules_done"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Modules Total</td>
+                            <td>
+                                <xsl:value-of select="Result/Summary/@modules_total"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Fingerprint</td>
+                            <td>
+                                <xsl:value-of select="Result/Build/@build_fingerprint"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Security Patch</td>
+                            <td>
+                                <xsl:value-of select="Result/Build/@build_version_security_patch"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">Release (SDK)</td>
+                            <td>
+                                <xsl:value-of select="Result/Build/@build_version_release"/> (<xsl:value-of select="Result/Build/@build_version_sdk"/>)
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="rowtitle">ABIs</td>
+                            <td>
+                                <xsl:value-of select="Result/Build/@build_abis"/>
+                            </td>
+                        </tr>
+                    </table>
+                </div>
+
+                <!-- High level summary of test execution -->
+                <br/>
+                <div>
+                    <table class="testsummary">
+                        <tr>
+                            <th>Module</th>
+                            <th>Passed</th>
+                            <th>Failed</th>
+                            <th>Not Executed</th>
+                            <th>Total Tests</th>
+                        </tr>
+                        <xsl:for-each select="Result/Module">
+                            <tr>
+                                <td>
+                                    <xsl:variable name="href"><xsl:value-of select="@name"/> - <xsl:value-of select="@abi"/></xsl:variable>
+                                    <a href="#{$href}"><xsl:value-of select="@name"/> - <xsl:value-of select="@abi"/></a>
+                                </td>
+                                <td>
+                                    <xsl:value-of select="count(TestCase/Test[@result = 'pass'])"/>
+                                </td>
+                                <td>
+                                    <xsl:value-of select="count(TestCase/Test[@result = 'fail'])"/>
+                                </td>
+                                <td>
+                                    <xsl:value-of select="@not_executed"/>
+                                </td>
+                                <td>
+                                    <xsl:value-of select="count(TestCase/Test) + @not_executed"/>
+                                </td>
+                            </tr>
+                        </xsl:for-each> <!-- end Module -->
+                    </table>
+                </div>
+
+                <xsl:call-template name="filteredResultTestReport">
+                    <xsl:with-param name="header" select="'Failed Tests'" />
+                    <xsl:with-param name="resultFilter" select="'fail'" />
+                </xsl:call-template>
+
+                <xsl:call-template name="filteredResultTestReport">
+                    <xsl:with-param name="header" select="'Not Executed Tests'" />
+                    <xsl:with-param name="resultFilter" select="'not_executed'" />
+                </xsl:call-template>
+
+                <br/>
+                <xsl:call-template name="detailedTestReport" />
+
+            </body>
+        </html>
+    </xsl:template>
+
+    <xsl:template name="filteredResultTestReport">
+        <xsl:param name="header" />
+        <xsl:param name="resultFilter" />
+        <xsl:variable name="numMatching" select="count(Result/Module/TestCase/Test[@result=$resultFilter])" />
+        <xsl:if test="$numMatching &gt; 0">
+            <h2 align="center"><xsl:value-of select="$header" /> (<xsl:value-of select="$numMatching"/>)</h2>
+            <xsl:call-template name="detailedTestReport">
+                <xsl:with-param name="resultFilter" select="$resultFilter"/>
+                <xsl:with-param name="fullStackTrace" select="true()"/>
+            </xsl:call-template>
+        </xsl:if>
+    </xsl:template>
+
+    <xsl:template name="detailedTestReport">
+        <xsl:param name="resultFilter" />
+        <xsl:param name="fullStackTrace" />
+        <div>
+            <xsl:for-each select="Result/Module">
+                <xsl:if test="$resultFilter=''
+                        or count(TestCase/Test[@result=$resultFilter]) &gt; 0">
+
+                    <table class="testdetails">
+                        <tr>
+                            <td class="module" colspan="3">
+                                <xsl:variable name="href"><xsl:value-of select="@name"/> - <xsl:value-of select="@abi"/></xsl:variable>
+                                <a name="{$href}"><xsl:value-of select="@name"/> - <xsl:value-of select="@abi"/></a>
+                            </td>
+                        </tr>
+
+                        <tr>
+                            <th width="30%">Test</th>
+                            <th width="5%">Result</th>
+                            <th>Details</th>
+                        </tr>
+
+                        <xsl:for-each select="TestCase">
+                            <xsl:variable name="TestCase" select="."/>
+                            <!-- test -->
+                            <xsl:for-each select="Test">
+                                <xsl:if test="$resultFilter='' or @result=$resultFilter">
+                                    <tr>
+                                        <td class="testname"> <xsl:value-of select="$TestCase/@name"/>#<xsl:value-of select="@name"/></td>
+
+                                        <!-- test results -->
+                                        <xsl:if test="@result='pass'">
+                                            <td class="pass">
+                                                <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                    <xsl:value-of select="@result"/>
+                                                </div>
+                                            </td>
+                                            <td class="failuredetails"/>
+                                        </xsl:if>
+
+                                        <xsl:if test="@result='fail'">
+                                            <td class="failed">
+                                                <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                    <xsl:value-of select="@result"/>
+                                                </div>
+                                            </td>
+                                            <td class="failuredetails">
+                                                <div class="details">
+                                                    <xsl:choose>
+                                                        <xsl:when test="$fullStackTrace=true()">
+                                                            <xsl:value-of select="Failure/StackTrace" />
+                                                        </xsl:when>
+                                                        <xsl:otherwise>
+                                                            <xsl:value-of select="Failure/@message"/>
+                                                        </xsl:otherwise>
+                                                    </xsl:choose>
+                                                </div>
+                                            </td>
+                                        </xsl:if>
+
+                                        <xsl:if test="@result='not_executed'">
+                                            <td class="not_executed">
+                                                <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                    <xsl:value-of select="@result"/>
+                                                </div>
+                                            </td>
+                                            <td class="failuredetails"></td>
+                                        </xsl:if>
+                                    </tr> <!-- finished with a row -->
+                                </xsl:if>
+                            </xsl:for-each> <!-- end test -->
+                        </xsl:for-each>
+                    </table>
+                </xsl:if>
+            </xsl:for-each> <!-- end test Module -->
+        </div>
+    </xsl:template>
+
+    <!-- Take a delimited string and insert line breaks after a some number of elements. -->
+    <xsl:template name="formatDelimitedString">
+        <xsl:param name="string" />
+        <xsl:param name="numTokensPerRow" select="10" />
+        <xsl:param name="tokenIndex" select="1" />
+        <xsl:if test="$string">
+            <!-- Requires the last element to also have a delimiter after it. -->
+            <xsl:variable name="token" select="substring-before($string, ';')" />
+            <xsl:value-of select="$token" />
+            <xsl:text>&#160;</xsl:text>
+
+            <xsl:if test="$tokenIndex mod $numTokensPerRow = 0">
+                <br />
+            </xsl:if>
+
+            <xsl:call-template name="formatDelimitedString">
+                <xsl:with-param name="string" select="substring-after($string, ';')" />
+                <xsl:with-param name="numTokensPerRow" select="$numTokensPerRow" />
+                <xsl:with-param name="tokenIndex" select="$tokenIndex + 1" />
+            </xsl:call-template>
+        </xsl:if>
+    </xsl:template>
+
+</xsl:stylesheet>
diff --git a/apps/CtsVerifier/assets/report/logo.png b/apps/CtsVerifier/assets/report/logo.png
new file mode 100644
index 0000000..61970b3
--- /dev/null
+++ b/apps/CtsVerifier/assets/report/logo.png
Binary files differ
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java b/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java
index 33c9b62..b6908a9 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ReportExporter.java
@@ -22,10 +22,20 @@
 import android.os.Build;
 import android.os.Environment;
 
+import com.android.compatibility.common.util.FileUtil;
+import com.android.compatibility.common.util.IInvocationResult;
+import com.android.compatibility.common.util.InvocationResult;
+import com.android.compatibility.common.util.ResultHandler;
+import com.android.compatibility.common.util.ZipUtil;
+
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.lang.System;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
@@ -38,6 +48,20 @@
  * 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 = "";
+    private static final String LOG_URL = null;
+    private static final String REFERENCE_URL = null;
+    private static final String SUITE_NAME_METADATA_KEY = "SuiteName";
+    private static final String SUITE_PLAN = "verifier";
+    private static final String SUITE_BUILD = "0";
+
+    private static final long START_MS = System.currentTimeMillis();
+    private static final long END_MS = START_MS;
+
+    private static final String REPORT_DIRECTORY = "verifierReports";
+    private static final String ZIP_EXTENSION = ".zip";
+
     protected static final Logger LOG = Logger.getLogger(ReportExporter.class.getName());
 
     private final Context mContext;
@@ -54,50 +78,80 @@
             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();
 
-        String baseName = getReportBaseName();
-        File reportFile = new File(reportPath, baseName + ".zip");
-        ZipOutputStream out = null;
+        String suiteName = Version.getMetadata(mContext, SUITE_NAME_METADATA_KEY);
+        // create a temporary directory for this particular report
+        File tempDir = new File(verifierReportsDir, getReportName(suiteName));
+        tempDir.mkdirs();
+
+        // create a File object for a report ZIP file
+        File reportZipFile = new File(
+                verifierReportsDir, getReportName(suiteName) + ZIP_EXTENSION);
+
         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(suiteName, versionName, SUITE_PLAN, SUITE_BUILD,
+                    result, tempDir, START_MS, END_MS, REFERENCE_URL, LOG_URL,
+                    COMMAND_LINE_ARGS);
+
+            // copy formatting files to the temporary report directory
+            copyFormattingFiles(tempDir);
+
+            // create a compressed ZIP file containing the temporary report directory
+            ZipUtil.createZip(tempDir, reportZipFile);
+        } 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 directory and its files made for the report
+            FileUtil.recursiveDelete(tempDir);
         }
-
-        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);
+    /**
+     * Copy the XML formatting files stored in the assets directory to the result output.
+     *
+     * @param resultsDir
+     */
+    private void copyFormattingFiles(File resultsDir) {
+        for (String resultFileName : ResultHandler.RESULT_RESOURCES) {
+            InputStream rawStream = null;
+            try {
+                rawStream = mContext.getAssets().open(
+                        String.format("report/%s", resultFileName));
+            } catch (IOException e) {
+                LOG.log(Level.WARNING, "Failed to load " + resultFileName + " from assets.");
+            }
+            if (rawStream != null) {
+                File resultFile = new File(resultsDir, resultFileName);
+                try {
+                    FileUtil.writeToFile(rawStream, resultFile);
+                } catch (IOException e) {
+                    LOG.log(Level.WARNING, "Failed to write " + resultFileName + " to a file.");
+                }
+            }
+        }
+    }
+
+    private String getReportName(String suiteName) {
+        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-%s",
+                date, suiteName, 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/apps/CtsVerifier/src/com/android/cts/verifier/Version.java b/apps/CtsVerifier/src/com/android/cts/verifier/Version.java
index e7b6121..272fbcd 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/Version.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/Version.java
@@ -17,12 +17,18 @@
 package com.android.cts.verifier;
 
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
 
 class Version {
 
+    private static final String TAG = Version.class.getSimpleName();
+
+    private static final String UNKNOWN = "unknown";
+
     static String getVersionName(Context context) {
         return getPackageInfo(context).versionName;
     }
@@ -40,4 +46,19 @@
                     + context.getPackageName());
         }
     }
+
+    static String getMetadata(Context context, String name) {
+        try {
+            PackageManager packageManager = context.getPackageManager();
+            ApplicationInfo applicationInfo = packageManager.getApplicationInfo(
+                    context.getPackageName(), PackageManager.GET_META_DATA);
+            String value = applicationInfo.metaData.getString(name);
+            if (value != null) {
+                return value;
+            }
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Version.getMetadata: " + name, e);
+        }
+        return UNKNOWN;
+    }
 }
diff --git a/common/device-side/util/Android.mk b/common/device-side/util/Android.mk
index 350c2db..8eb125c 100644
--- a/common/device-side/util/Android.mk
+++ b/common/device-side/util/Android.mk
@@ -24,7 +24,8 @@
 
 LOCAL_MODULE := compatibility-device-util
 
-LOCAL_SDK_VERSION := current
+# uncomment when b/13282254 is fixed
+#LOCAL_SDK_VERSION := current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
diff --git a/common/device-side/util/src/com/android/compatibility/common/util/PropertyUtil.java b/common/device-side/util/src/com/android/compatibility/common/util/PropertyUtil.java
new file mode 100644
index 0000000..1a1ec19
--- /dev/null
+++ b/common/device-side/util/src/com/android/compatibility/common/util/PropertyUtil.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 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 android.os.Build;
+import android.os.SystemProperties;
+
+/**
+ * Device-side utility class for reading properties and gathering information for testing
+ * Android device compatibility.
+ */
+public class PropertyUtil {
+
+    /**
+     * Name of read-only property detailing the first API level for which the product was
+     * shipped. Property should be undefined for factory ROM products.
+     */
+    public static String FIRST_API_LEVEL = "ro.product.first_api_level";
+
+    /** Value to be returned by getPropertyInt() if property is not found */
+    public static int INT_VALUE_IF_UNSET = -1;
+
+    /** Returns whether the device build is the factory ROM */
+    public static boolean isFactoryROM() {
+        // property should be undefined if and only if the product is factory ROM.
+        return getPropertyInt(FIRST_API_LEVEL) == INT_VALUE_IF_UNSET;
+    }
+
+    /**
+     * Return the first API level for this product. If the read-only property is unset,
+     * this means the first API level is the current API level, and the current API level
+     * is returned.
+     */
+    public static int getFirstApiLevel() {
+        int firstApiLevel = getPropertyInt(FIRST_API_LEVEL);
+        return (firstApiLevel == INT_VALUE_IF_UNSET) ? Build.VERSION.SDK_INT : firstApiLevel;
+    }
+
+    /**
+     * Retrieves the desired integer property, returning INT_VALUE_IF_UNSET if not found.
+     */
+    public static int getPropertyInt(String property) {
+        return SystemProperties.getInt(property, INT_VALUE_IF_UNSET);
+    }
+}
diff --git a/common/host-side/tradefed/res/config/metadata-config.xml b/common/host-side/tradefed/res/config/metadata-config.xml
new file mode 100644
index 0000000..37f1a3e
--- /dev/null
+++ b/common/host-side/tradefed/res/config/metadata-config.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<configuration description="Metadata result reporter for Compatibility suites">
+    <result_reporter class="com.android.compatibility.common.tradefed.result.MetadataReporter" />
+</configuration>
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java
index 235d71b..73f8638 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/build/CompatibilityBuildHelper.java
@@ -33,15 +33,16 @@
 
     public static final String MODULE_IDS = "MODULE_IDS";
 
-    private static final String ROOT_DIR = "ROOT_DIR";
+    public static final String ROOT_DIR = "ROOT_DIR";
+    public static final String SUITE_NAME = "SUITE_NAME";
+    public static final String START_TIME_MS = "START_TIME_MS";
+
     private static final String ROOT_DIR2 = "ROOT_DIR2";
     private static final String SUITE_BUILD = "SUITE_BUILD";
-    private static final String SUITE_NAME = "SUITE_NAME";
     private static final String SUITE_FULL_NAME = "SUITE_FULL_NAME";
     private static final String SUITE_VERSION = "SUITE_VERSION";
     private static final String SUITE_PLAN = "SUITE_PLAN";
     private static final String RESULT_DIR = "RESULT_DIR";
-    private static final String START_TIME_MS = "START_TIME_MS";
     private static final String CONFIG_PATH_PREFIX = "DYNAMIC_CONFIG_FILE:";
     private static final String DYNAMIC_CONFIG_OVERRIDE_URL = "DYNAMIC_CONFIG_OVERRIDE_URL";
     private static final String COMMAND_LINE_ARGS = "command_line_args";
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/MetadataReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/MetadataReporter.java
new file mode 100644
index 0000000..5f94a88
--- /dev/null
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/MetadataReporter.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2017 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.tradefed.result;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.json.stream.JsonWriter;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionCopier;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.IShardableListener;
+import com.android.tradefed.result.StubTestInvocationListener;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Map;
+
+/**
+ * Write test metadata to the result/metadata folder.
+ */
+public class MetadataReporter extends StubTestInvocationListener implements IShardableListener {
+
+    @Option(name = "include-failure-time", description = "Include timing about tests that failed.")
+    private boolean mIncludeFailures = false;
+
+    @Option(name = "min-test-duration", description = "Ignore test durations less than this.",
+            isTimeVal = true)
+    private long mMinTestDuration = 2 * 1000;
+
+    private static final String METADATA_DIR = "metadata";
+    private CompatibilityBuildHelper mBuildHelper;
+    private File mMetadataDir;
+    private long mStartTime;
+    private String mCurrentModule;
+    private boolean mTestFailed;
+    private Collection<TestMetadata> mTestMetadata = new LinkedList<>();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public IShardableListener clone() {
+        MetadataReporter clone = new MetadataReporter();
+        OptionCopier.copyOptionsNoThrow(this, clone);
+        return clone;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void invocationStarted(IBuildInfo buildInfo) {
+        if (buildInfo == null) {
+            throw new RuntimeException("buildInfo is null");
+        }
+        synchronized(this) {
+            if (mBuildHelper == null) {
+                mBuildHelper = new CompatibilityBuildHelper(buildInfo);
+                try {
+                    mMetadataDir = new File(mBuildHelper.getResultDir(), METADATA_DIR);
+                } catch (FileNotFoundException e) {
+                    throw new RuntimeException("Metadata Directory was not created: " +
+                            mMetadataDir.getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testRunStarted(String id, int numTests) {
+        this.mCurrentModule = id;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testStarted(TestIdentifier test) {
+        mStartTime = System.currentTimeMillis();
+        mTestFailed = false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testFailed(TestIdentifier test, String trace) {
+        mTestFailed = true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testIgnored(TestIdentifier test) {
+        mTestFailed = true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testAssumptionFailure(TestIdentifier test, String trace) {
+        mTestFailed = true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+        long duration = System.currentTimeMillis() - mStartTime;
+        if (mTestFailed && !mIncludeFailures) {
+            return;
+        }
+        if (duration < mMinTestDuration) {
+            return;
+        }
+
+        TestMetadata metadata = new TestMetadata();
+        metadata.testId = buildTestId(test);
+        metadata.seconds = duration / 1000; // convert to second for reporting
+        mTestMetadata.add(metadata);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void testRunEnded(long elapsedTime, Map<String, String> metrics) {
+        if (!mTestMetadata.isEmpty()) {
+            tryWriteToFile(mBuildHelper, mCurrentModule, mMetadataDir, mTestMetadata);
+        }
+        mTestMetadata.clear();
+    }
+
+    /** Information about a test's execution. */
+    public static class TestMetadata {
+        // The id of the test
+        String testId;
+        // The duration of the test.
+        long seconds;
+    }
+
+    private static String buildTestId(TestIdentifier test) {
+        return String.format("%s.%s", test.getClassName(), test.getTestName());
+    }
+
+    private static void tryWriteToFile(
+            CompatibilityBuildHelper compatibilityBuildHelper,
+            String moduleName,
+            File metadataDir,
+            Collection<TestMetadata> metadatas) {
+
+        metadataDir.mkdirs();
+
+        String moduleFileName = moduleName + "." + System.currentTimeMillis() + ".json";
+        File metadataFile = new File(metadataDir, moduleFileName);
+        Map<String, String> buildAttributes =
+                compatibilityBuildHelper.getBuildInfo().getBuildAttributes();
+        try (JsonWriter writer = new JsonWriter(new PrintWriter(metadataFile))) {
+            writer.beginObject();
+
+            writer.name("fingerprint");
+            writer.value(buildAttributes.get("cts:build_fingerprint"));
+
+            writer.name("product");
+            writer.value(buildAttributes.get("cts:build_product"));
+
+            writer.name("build_id");
+            writer.value(buildAttributes.get("cts:build_id"));
+
+            writer.name("suite_version");
+            writer.value(compatibilityBuildHelper.getSuiteVersion());
+
+            writer.name("suite_name");
+            writer.value(compatibilityBuildHelper.getSuiteName());
+
+            writer.name("suite_build");
+            writer.value(compatibilityBuildHelper.getSuiteBuild());
+
+            writer.name("module_id");
+            writer.value(moduleName);
+
+            writer.name("test");
+            writer.beginArray();
+            for (TestMetadata metadata : metadatas) {
+                writer.beginObject();
+                writer.name("id");
+                writer.value(metadata.testId);
+                writer.name("sec");
+                writer.value(metadata.seconds);
+                writer.endObject();
+            }
+            writer.endArray();
+
+            writer.endObject();
+        } catch (IOException e) {
+            CLog.e("[%s] While saving metadata.", metadataFile.getAbsolutePath());
+            CLog.e(e);
+        }
+    }
+
+    protected Collection<TestMetadata> getTestMetadata() {
+        return Collections.unmodifiableCollection(mTestMetadata);
+    }
+}
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
index 620fa0a..8136f23 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
@@ -77,11 +77,6 @@
     private static final String RESULT_KEY = "COMPATIBILITY_TEST_RESULT";
     private static final String CTS_PREFIX = "cts:";
     private static final String BUILD_INFO = CTS_PREFIX + "build_";
-    private static final String[] RESULT_RESOURCES = {
-        "compatibility_result.css",
-        "compatibility_result.xsd",
-        "compatibility_result.xsl",
-        "logo.png"};
 
     @Option(name = CompatibilityTest.RETRY_OPTION,
             shortName = 'r',
@@ -502,6 +497,10 @@
                     mBuildHelper.getSuiteBuild(), mResult, mResultDir, startTime,
                     elapsedTime + startTime, mReferenceUrl, getLogUrl(),
                     mBuildHelper.getCommandLineArgs());
+            if (mRetrySessionId != null) {
+                copyRetryFiles(ResultHandler.getResultDirectory(
+                        mBuildHelper.getResultsDir(), mRetrySessionId), mResultDir);
+            }
             File zippedResults = zipResults(mResultDir);
 
             // Create failure report after zip file so extra data is not uploaded
@@ -681,7 +680,7 @@
      * @param resultsDir
      */
     static void copyFormattingFiles(File resultsDir) {
-        for (String resultFileName : RESULT_RESOURCES) {
+        for (String resultFileName : ResultHandler.RESULT_RESOURCES) {
             InputStream configStream = ResultHandler.class.getResourceAsStream(
                     String.format("/report/%s", resultFileName));
             if (configStream != null) {
@@ -720,6 +719,33 @@
     }
 
     /**
+     * Recursively copy any other files found in the previous session's result directory to the
+     * new result directory, so long as they don't already exist. For example, a "screenshots"
+     * directory generated in a previous session by a passing test will not be generated on retry
+     * unless copied from the old result directory.
+     *
+     * @param oldResultsDir
+     * @param newResultsDir
+     */
+    static void copyRetryFiles(File oldResultsDir, File newResultsDir) {
+        File[] oldFiles = oldResultsDir.listFiles();
+        for (File oldFile : oldFiles) {
+            File newFile = new File (newResultsDir, oldFile.getName());
+            if (!newFile.exists()) {
+                try {
+                    if (oldFile.isDirectory()) {
+                        FileUtil.recursiveCopy(oldFile, newFile);
+                    } else {
+                        FileUtil.copyFile(oldFile, newFile);
+                    }
+                } catch (IOException e) {
+                    warn("Failed to copy file \"%s\" from previous session", oldFile.getName());
+                }
+            }
+        }
+    }
+
+    /**
      * Zip the contents of the given results directory.
      *
      * @param resultsDir
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/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java
index 3b1c5e7..f63093c 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/CompatibilityTest.java
@@ -50,7 +50,6 @@
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.targetprep.ITargetPreparer;
 import com.android.tradefed.testtype.IAbi;
 import com.android.tradefed.testtype.IBuildReceiver;
@@ -71,7 +70,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -429,14 +427,8 @@
 
                 // execute pre module execution checker
                 runPreModuleCheck(module.getName(), checkers, mDevice, listener);
-                // Workaround to b/34202787: Add result forwarder that ensures module is reported
-                // with 0 tests if test runner doesn't report anything in this case.
-                // Necessary for solution to b/33289177, in which completed modules may sometimes
-                // not be marked done until retried with 0 tests.
-                ModuleResultForwarder moduleListener = new ModuleResultForwarder(listener);
                 try {
-                    module.run(moduleListener);
-                    moduleListener.finish(module.getId());
+                    module.run(listener);
                 } catch (DeviceUnresponsiveException due) {
                     // being able to catch a DeviceUnresponsiveException here implies that recovery
                     // was successful, and test execution should proceed to next module
@@ -753,32 +745,4 @@
 
         return shardQueue;
     }
-
-    private class ModuleResultForwarder extends ResultForwarder {
-
-        private boolean mTestRunStarted = false;
-        private ITestInvocationListener mListener;
-
-        public ModuleResultForwarder(ITestInvocationListener listener) {
-            super(listener);
-            mListener = listener;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void testRunStarted(String name, int numTests) {
-            mListener.testRunStarted(name, numTests);
-            mTestRunStarted = true;
-        }
-
-        public void finish(String moduleId) {
-            if (!mTestRunStarted) {
-                mListener.testRunStarted(moduleId, 0);
-                mListener.testRunEnded(0, Collections.emptyMap());
-            }
-        }
-    }
-
 }
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleDef.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleDef.java
index d8a2adb..c69b3a7 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleDef.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/testtype/ModuleDef.java
@@ -28,6 +28,7 @@
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ResultForwarder;
 import com.android.tradefed.targetprep.BuildError;
 import com.android.tradefed.targetprep.ITargetCleaner;
 import com.android.tradefed.targetprep.ITargetPreparer;
@@ -219,7 +220,6 @@
      */
     @Override
     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        IModuleListener moduleListener = new ModuleListener(this, listener);
         // Run DynamicConfigPusher setup once more, in case cleaner has previously
         // removed dynamic config file from the target (see b/32877809)
         for (ITargetPreparer preparer : mDynamicConfigPreparers) {
@@ -241,7 +241,11 @@
             ((IDeviceTest) mTest).setDevice(mDevice);
         }
 
-        mTest.run(moduleListener);
+        IModuleListener moduleListener = new ModuleListener(this, listener);
+        // Guarantee events testRunStarted and testRunEnded in case underlying test runner does not
+        ModuleFinisher moduleFinisher = new ModuleFinisher(moduleListener);
+        mTest.run(moduleFinisher);
+        moduleFinisher.finish();
 
         // Tear down
         for (ITargetCleaner cleaner : mCleaners) {
@@ -309,4 +313,37 @@
             e.printStackTrace();
         }
     }
+
+    /**
+     * ResultForwarder that tracks whether method testRunStarted() has been called for its
+     * listener. If not, invoking finish() will call testRunStarted with 0 tests for this module,
+     * as well as testRunEnded with 0 ms elapsed.
+     */
+    private class ModuleFinisher extends ResultForwarder {
+
+        private boolean mFinished;
+        private ITestInvocationListener mListener;
+
+        public ModuleFinisher(ITestInvocationListener listener) {
+            super(listener);
+            mListener = listener;
+            mFinished = false;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void testRunStarted(String name, int numTests) {
+            mListener.testRunStarted(name, numTests);
+            mFinished = true;
+        }
+
+        public void finish() {
+            if (!mFinished) {
+                mListener.testRunStarted(mId, 0);
+                mListener.testRunEnded(0, Collections.emptyMap());
+            }
+        }
+    }
 }
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
index b8b7858..860c830 100644
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/UnitTests.java
@@ -19,6 +19,7 @@
 import com.android.compatibility.common.tradefed.command.CompatibilityConsoleTest;
 import com.android.compatibility.common.tradefed.result.ChecksumReporterTest;
 import com.android.compatibility.common.tradefed.result.ConsoleReporterTest;
+import com.android.compatibility.common.tradefed.result.MetadataReporterTest;
 import com.android.compatibility.common.tradefed.result.ResultReporterTest;
 import com.android.compatibility.common.tradefed.result.SubPlanCreatorTest;
 import com.android.compatibility.common.tradefed.targetprep.PropertyCheckTest;
@@ -51,6 +52,7 @@
         addTestSuite(CompatibilityTestTest.class);
         addTestSuite(OptionHelperTest.class);
         addTestSuite(CollectorUtilTest.class);
+        addTestSuite(MetadataReporterTest.class);
         addTestSuite(ModuleDefTest.class);
         addTestSuite(ModuleRepoTest.class);
         addTestSuite(PropertyCheckTest.class);
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/MetadataReporterTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/MetadataReporterTest.java
new file mode 100644
index 0000000..dba3128
--- /dev/null
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/MetadataReporterTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 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.tradefed.result;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.util.AbiUtils;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.RunUtil;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.HashMap;
+
+/**
+ * Unit Tests for {@link MetadataReporter}
+ */
+public class MetadataReporterTest extends TestCase {
+
+    private static final String MIN_TEST_DURATION = "10";
+    private static final String BUILD_NUMBER = "2";
+    private static final String SUITE_PLAN = "cts";
+    private static final String DYNAMIC_CONFIG_URL = "";
+    private static final String ROOT_DIR_NAME = "root";
+    private static final String BASE_DIR_NAME = "android-tests";
+    private static final String TESTCASES = "testcases";
+    private static final String NAME = "ModuleName";
+    private static final String ABI = "mips64";
+    private static final String ID = AbiUtils.createId(ABI, NAME);
+    private static final String CLASS = "android.test.FoorBar";
+    private static final String METHOD_1 = "testBlah1";
+    private static final String METHOD_2 = "testBlah2";
+    private static final String METHOD_3 = "testBlah3";
+    private static final String STACK_TRACE = "Something small is not alright\n " +
+            "at four.big.insects.Marley.sing(Marley.java:10)";
+    private static final long START_TIME = 123456L;
+
+    private MetadataReporter mReporter;
+    private IBuildInfo mBuildInfo;
+    private CompatibilityBuildHelper mBuildHelper;
+
+    private File mRoot = null;
+    private File mBase = null;
+    private File mTests = null;
+
+    @Override
+    public void setUp() throws Exception {
+        mReporter = new MetadataReporter();
+        OptionSetter setter = new OptionSetter(mReporter);
+        setter.setOptionValue("min-test-duration", MIN_TEST_DURATION);
+        mRoot = FileUtil.createTempDir(ROOT_DIR_NAME);
+        mBase = new File(mRoot, BASE_DIR_NAME);
+        mBase.mkdirs();
+        mTests = new File(mBase, TESTCASES);
+        mTests.mkdirs();
+        System.setProperty(CompatibilityBuildHelper.ROOT_DIR, mRoot.getAbsolutePath());
+        mBuildInfo = new BuildInfo(BUILD_NUMBER, "", "");
+        mBuildHelper = new CompatibilityBuildHelper(mBuildInfo);
+        mBuildHelper.init(SUITE_PLAN, DYNAMIC_CONFIG_URL, START_TIME);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mReporter = null;
+        FileUtil.recursiveDelete(mRoot);
+    }
+
+    /**
+     * Test that when tests execute faster than the threshold we do not report then.
+     */
+    public void testResultReportingFastTests() throws Exception {
+        mReporter.invocationStarted(mBuildInfo);
+        mReporter.testRunStarted(ID, 3);
+        runTests(0l);
+        Collection<MetadataReporter.TestMetadata> metadata = mReporter.getTestMetadata();
+        assertTrue(metadata.isEmpty());
+        mReporter.testRunEnded(10, new HashMap<String, String>());
+        mReporter.invocationEnded(10);
+    }
+
+    /**
+     * Test that when tests execute slower than the limit we report them if they passed.
+     */
+    public void testResultReportingSlowTests() throws Exception {
+        mReporter.invocationStarted(mBuildInfo);
+        mReporter.testRunStarted(ID, 3);
+        runTests(50l);
+
+        Collection<MetadataReporter.TestMetadata> metadata = mReporter.getTestMetadata();
+        assertEquals(metadata.size(), 2); // Two passing slow tests.
+
+        mReporter.testRunEnded(10, new HashMap<String, String>());
+        mReporter.invocationEnded(10);
+    }
+
+    /** Run 4 test. */
+    private void runTests(long waitTime) {
+        TestIdentifier test1 = new TestIdentifier(CLASS, METHOD_1);
+        mReporter.testStarted(test1);
+        RunUtil.getDefault().sleep(waitTime);
+        mReporter.testEnded(test1, new HashMap<String, String>());
+
+        TestIdentifier test2 = new TestIdentifier(CLASS, METHOD_2);
+        mReporter.testStarted(test2);
+        RunUtil.getDefault().sleep(waitTime);
+        mReporter.testEnded(test1, new HashMap<String, String>());
+
+        TestIdentifier test3 = new TestIdentifier(CLASS, METHOD_3);
+        mReporter.testStarted(test3);
+        RunUtil.getDefault().sleep(waitTime);
+        mReporter.testFailed(test3, STACK_TRACE);
+        mReporter.testEnded(test3, new HashMap<String, String>());
+
+        TestIdentifier test4 = new TestIdentifier(CLASS, METHOD_3);
+        mReporter.testStarted(test4);
+        RunUtil.getDefault().sleep(waitTime);
+        mReporter.testIgnored(test4);
+        mReporter.testEnded(test4, new HashMap<String, String>());
+    }
+}
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleDefTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleDefTest.java
index 2ab884e..019557e 100644
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleDefTest.java
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/testtype/ModuleDefTest.java
@@ -28,9 +28,12 @@
 import com.android.tradefed.testtype.ITestCollector;
 import com.android.tradefed.testtype.ITestFilterReceiver;
 
+import org.easymock.EasyMock;
+
 import junit.framework.TestCase;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -52,6 +55,21 @@
         assertEquals("Incorrect Name", NAME, def.getName());
     }
 
+    public void testModuleFinisher() throws Exception {
+        IAbi abi = new Abi(ABI, "");
+        MockRemoteTest mockTest = new MockRemoteTest();
+        IModuleDef def = new ModuleDef(NAME, abi, mockTest, new ArrayList<ITargetPreparer>());
+        ITestInvocationListener mockListener = EasyMock.createMock(ITestInvocationListener.class);
+        // listener should receive testRunStarted/testRunEnded events even for no-op run() method
+        mockListener.testRunStarted(ID, 0);
+        EasyMock.expectLastCall().once();
+        mockListener.testRunEnded(0, Collections.emptyMap());
+        EasyMock.expectLastCall().once();
+        EasyMock.replay(mockListener);
+        def.run(mockListener);
+        EasyMock.verify(mockListener);
+    }
+
     private class MockRemoteTest implements IRemoteTest, ITestFilterReceiver, IAbiReceiver,
             IRuntimeHintProvider, ITestCollector {
 
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/PropertyUtil.java b/common/host-side/util/src/com/android/compatibility/common/util/PropertyUtil.java
new file mode 100644
index 0000000..199b826
--- /dev/null
+++ b/common/host-side/util/src/com/android/compatibility/common/util/PropertyUtil.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 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 com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+/**
+ * Host-side utility class for reading properties and gathering information for testing
+ * Android device compatibility.
+ */
+public class PropertyUtil {
+
+    /**
+     * Name of read-only property detailing the first API level for which the product was
+     * shipped. Property should be undefined for factory ROM products.
+     */
+    public static String FIRST_API_LEVEL = "ro.product.first_api_level";
+
+    /** Returns whether the device build is the factory ROM */
+    public static boolean isFactoryROM(ITestDevice device) throws DeviceNotAvailableException {
+        // first API level property should be undefined if and only if the product is factory ROM.
+        return device.getProperty(FIRST_API_LEVEL) == null;
+    }
+
+    /**
+     * Return the first API level for this product. If the read-only property is unset,
+     * this means the first API level is the current API level, and the current API level
+     * is returned.
+     */
+    public static int getFirstApiLevel(ITestDevice device) throws DeviceNotAvailableException {
+        String propString = device.getProperty(FIRST_API_LEVEL);
+        return (propString == null) ? device.getApiLevel() : Integer.parseInt(propString);
+    }
+}
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java b/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
index 169cfdb..094943b 100644
--- a/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
+++ b/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
@@ -28,10 +28,9 @@
     public HostUnitTests() {
         super();
         addTestSuite(DynamicConfigHandlerTest.class);
-        addTestSuite(ResultHandlerTest.class);
     }
 
     public static Test suite() {
         return new HostUnitTests();
     }
-}
\ No newline at end of file
+}
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/FileUtil.java b/common/util/src/com/android/compatibility/common/util/FileUtil.java
new file mode 100644
index 0000000..b59912b
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/FileUtil.java
@@ -0,0 +1,71 @@
+/*
+ * 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.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A helper class for file related operations
+ */
+public class FileUtil {
+
+    /**
+     * Recursively delete given file or directory and all its contents.
+     *
+     * @param rootDir the directory or file to be deleted; can be null
+     */
+    public static void recursiveDelete(File rootDir) {
+        if (rootDir != null) {
+            if (rootDir.isDirectory()) {
+                File[] childFiles = rootDir.listFiles();
+                if (childFiles != null) {
+                    for (File child : childFiles) {
+                        recursiveDelete(child);
+                    }
+                }
+            }
+            rootDir.delete();
+        }
+    }
+
+    /**
+     * A helper method for writing stream data to file
+     *
+     * @param input the unbuffered input stream
+     * @param destFile the dest file to write to
+     */
+    public static void writeToFile(InputStream input, File destFile) throws IOException {
+        InputStream origStream = null;
+        OutputStream destStream = null;
+        try {
+            origStream = new BufferedInputStream(input);
+            destStream = new BufferedOutputStream(new FileOutputStream(destFile));
+            StreamUtil.copyStreams(origStream, destStream);
+        } finally {
+            origStream.close();
+            destStream.flush();
+            destStream.close();
+        }
+    }
+
+}
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 93%
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..6c2e98a 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,10 +58,17 @@
     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";
 
+    public static final String[] RESULT_RESOURCES = {
+        "compatibility_result.css",
+        "compatibility_result.xsd",
+        "compatibility_result.xsl",
+        "logo.png"
+    };
+
     // XML constants
     private static final String ABI_ATTR = "abi";
     private static final String BUGREPORT_TAG = "BugReport";
@@ -451,18 +456,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
                     }
@@ -496,6 +505,22 @@
     }
 
     /**
+     * Get the result directory for the given sessionId.
+     */
+    public static File getResultDirectory(File resultsDir, Integer sessionId) {
+        if (sessionId < 0) {
+            throw new IllegalArgumentException(
+                String.format("Invalid session id [%d] ", sessionId));
+        }
+        List<File> allResultDirs = getResultDirectories(resultsDir);
+        if (sessionId >= allResultDirs.size()) {
+            throw new IllegalArgumentException(String.format("Invalid session id [%d], results" +
+                    "directory contains only %d results", sessionId, allResultDirs.size()));
+        }
+        return allResultDirs.get(sessionId);
+    }
+
+    /**
      * Get a list of child directories that contain test invocation results
      * @param resultsDir the root test result directory
      * @return
diff --git a/common/util/src/com/android/compatibility/common/util/StreamUtil.java b/common/util/src/com/android/compatibility/common/util/StreamUtil.java
new file mode 100644
index 0000000..febd73d
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/StreamUtil.java
@@ -0,0 +1,46 @@
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class StreamUtil {
+
+    // 16K buffer size
+    private static final int BUFFER_SIZE = 16 * 1024;
+
+    /**
+     * Copies contents of origStream to destStream.
+     * <p/>
+     * Recommended to provide a buffered stream for input and output
+     *
+     * @param inStream the {@link InputStream}
+     * @param outStream the {@link OutputStream}
+     * @throws IOException
+     */
+    public static void copyStreams(InputStream inStream, OutputStream outStream)
+            throws IOException {
+        byte[] buf = new byte[BUFFER_SIZE];
+        int size = -1;
+        while ((size = inStream.read(buf)) != -1) {
+            outStream.write(buf, 0, size);
+        }
+    }
+
+}
diff --git a/common/util/src/com/android/compatibility/common/util/ZipUtil.java b/common/util/src/com/android/compatibility/common/util/ZipUtil.java
new file mode 100644
index 0000000..6cee83a
--- /dev/null
+++ b/common/util/src/com/android/compatibility/common/util/ZipUtil.java
@@ -0,0 +1,130 @@
+/*
+ * 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.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+public class ZipUtil {
+
+    /**
+     * Utility method to create a zip file containing the given directory and
+     * all its contents.
+     *
+     * @param dir the directory to zip
+     * @param zipFile the zip file to create - it should not already exist
+     * @throws IOException if failed to create zip file
+     */
+    public static void createZip(File dir, File zipFile) throws IOException {
+        ZipOutputStream out = null;
+        try {
+            FileOutputStream fileStream = new FileOutputStream(zipFile);
+            out = new ZipOutputStream(new BufferedOutputStream(fileStream));
+            addToZip(out, dir, new LinkedList<String>());
+        } catch (IOException e) {
+            zipFile.delete();
+            throw e;
+        } catch (RuntimeException e) {
+            zipFile.delete();
+            throw e;
+        } finally {
+            out.close();
+        }
+    }
+
+    /**
+     * Recursively adds given file and its contents to ZipOutputStream
+     *
+     * @param out the {@link ZipOutputStream}
+     * @param file the {@link File} to add to the stream
+     * @param relativePathSegs the relative path of file, including separators
+     * @throws IOException if failed to add file to zip
+     */
+    public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
+            throws IOException {
+        relativePathSegs.add(file.getName());
+        if (file.isDirectory()) {
+            // note: it appears even on windows, ZipEntry expects '/' as a path separator
+            relativePathSegs.add("/");
+        }
+        ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
+        out.putNextEntry(zipEntry);
+        if (file.isFile()) {
+            writeToStream(file, out);
+        }
+        out.closeEntry();
+        if (file.isDirectory()) {
+            // recursively add contents
+            File[] subFiles = file.listFiles();
+            if (subFiles == null) {
+                throw new IOException(String.format("Could not read directory %s",
+                        file.getAbsolutePath()));
+            }
+            for (File subFile : subFiles) {
+                addToZip(out, subFile, relativePathSegs);
+            }
+            // remove the path separator
+            relativePathSegs.remove(relativePathSegs.size()-1);
+        }
+        // remove the last segment, added at beginning of method
+        relativePathSegs.remove(relativePathSegs.size()-1);
+    }
+
+    /**
+     * Builds a file system path from a stack of relative path segments
+     *
+     * @param relativePathSegs the list of relative paths
+     * @return a {@link String} containing all relativePathSegs
+     */
+    private static String buildPath(List<String> relativePathSegs) {
+        StringBuilder pathBuilder = new StringBuilder();
+        for (String segment : relativePathSegs) {
+            pathBuilder.append(segment);
+        }
+        return pathBuilder.toString();
+    }
+
+    /**
+     * Helper method to write input file contents to output stream.
+     *
+     * @param file the input {@link File}
+     * @param out the {@link OutputStream}
+     *
+     * @throws IOException
+     */
+    private static void writeToStream(File file, OutputStream out) throws IOException {
+        InputStream inputStream = null;
+        try {
+            inputStream = new BufferedInputStream(new FileInputStream(file));
+            StreamUtil.copyStreams(inputStream, out);
+        } finally {
+            inputStream.close();
+        }
+    }
+
+}
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 96%
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..dcead0e 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)";
@@ -212,9 +207,8 @@
             String moduleBTest4 = String.format(XML_TEST_RESULT, METHOD_4,
                     SUMMARY_SOURCE, SUMMARY_MESSAGE, ResultType.HIGHER_BETTER.toReportString(),
                     ResultUnit.SCORE.toReportString(), Double.toString(SUMMARY_VALUE),
-                    DETAILS_SOURCE, DETAILS_MESSAGE, ResultType.LOWER_BETTER.toReportString(),
-                    ResultUnit.MS.toReportString(), Double.toString(DETAILS_VALUE_1),
-                    Double.toString(DETAILS_VALUE_2), Double.toString(DETAILS_VALUE_3));
+                    ResultType.LOWER_BETTER.toReportString(),
+                    ResultUnit.MS.toReportString());
             String moduleBTests = String.format(JOIN, moduleBTest3, moduleBTest4);
             String moduleBCases = String.format(XML_CASE, CLASS_B, moduleBTests);
             String moduleB = String.format(XML_MODULE, NAME_B, ABI, DEVICE_B, RUNTIME_B, DONE_B,
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/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileUtils.java b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileUtils.java
old mode 100644
new mode 100755
index 6a63cea..203c322
--- a/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileUtils.java
+++ b/hostsidetests/devicepolicy/app/ManagedProfile/src/com/android/cts/managedprofile/CrossProfileUtils.java
@@ -103,7 +103,7 @@
                getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
         Intent webIntent = new Intent(Intent.ACTION_VIEW);
         webIntent.setData(Uri.parse("http://com.android.cts.intent.receiver"));
-        List<ResolveInfo> ris = pm.queryIntentActivities(webIntent, 0 /* no flags*/);
+        List<ResolveInfo> ris = pm.queryIntentActivities(webIntent, PackageManager.MATCH_ALL /* all browser*/);
         for (ResolveInfo ri : ris) {
             Log.d(TAG, "Hiding " + ri.activityInfo.packageName);
             dpm.setApplicationHidden(ADMIN_RECEIVER_COMPONENT, ri.activityInfo.packageName, true);
diff --git a/hostsidetests/theme/assets/24/360dpi.zip b/hostsidetests/theme/assets/24/360dpi.zip
new file mode 100755
index 0000000..98782d5
--- /dev/null
+++ b/hostsidetests/theme/assets/24/360dpi.zip
Binary files differ
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 \
diff --git a/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java b/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java
index 7b74ba7..9b1dc81 100644
--- a/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java
+++ b/tests/tests/media/src/android/media/cts/DecodeAccuracyTest.java
@@ -19,11 +19,22 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.cts.util.MediaUtils;
 import android.graphics.Bitmap;
+import android.media.MediaFormat;
+import android.support.test.runner.AndroidJUnit4;
 import android.util.Log;
 import android.view.View;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.Timeout;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
 @TargetApi(24)
+@RunWith(AndroidJUnit4.class)
 public class DecodeAccuracyTest extends DecodeAccuracyTestBase {
 
     private static final String TAG = DecodeAccuracyTest.class.getSimpleName();
@@ -32,12 +43,17 @@
     private static final String H264_CROPPED_VIDEO_FILE_NAME = "520x360h264decodertest.mp4";
     private static final int ALLOWED_GREATEST_PIXEL_DIFFERENCE = 90;
     private static final int OFFSET = 10;
+    private static final int PER_TEST_TIMEOUT_S = 30;
 
     private View videoView;
     private VideoViewFactory videoViewFactory;
 
+    @Rule
+    public Timeout globalTimeout = Timeout.seconds(PER_TEST_TIMEOUT_S);
+
+    @After
     @Override
-    protected void tearDown() throws Exception {
+    public void tearDown() throws Exception {
         if (videoView != null) {
             getHelper().cleanUpView(videoView);
         }
@@ -48,36 +64,42 @@
     }
 
     /* <------------- Tests Using H264 -------------> */
+    @Test
     public void testH264GLViewVideoDecode() throws Exception {
         runH264DecodeAccuracyTest(
                 new GLSurfaceViewFactory(),
                 new VideoFormat(H264_VIDEO_FILE_NAME));
     }
 
+    @Test
     public void testH264GLViewLargerHeightVideoDecode() throws Exception {
         runH264DecodeAccuracyTest(
                 new GLSurfaceViewFactory(),
                 getLargerHeightVideoFormat(new VideoFormat(H264_VIDEO_FILE_NAME)));
     }
 
+    @Test
     public void testH264GLViewLargerWidthVideoDecode() throws Exception {
         runH264DecodeAccuracyTest(
                 new GLSurfaceViewFactory(),
                 getLargerWidthVideoFormat(new VideoFormat(H264_VIDEO_FILE_NAME)));
     }
 
+    @Test
     public void testH264SurfaceViewVideoDecode() throws Exception {
         runH264DecodeAccuracyTest(
                 new SurfaceViewFactory(),
                 new VideoFormat(H264_VIDEO_FILE_NAME));
     }
 
+    @Test
     public void testH264SurfaceViewLargerHeightVideoDecode() throws Exception {
         runH264DecodeAccuracyTest(
                 new SurfaceViewFactory(),
                 getLargerHeightVideoFormat(new VideoFormat(H264_VIDEO_FILE_NAME)));
     }
 
+    @Test
     public void testH264SurfaceViewLargerWidthVideoDecode() throws Exception {
         runH264DecodeAccuracyTest(
                 new SurfaceViewFactory(),
@@ -85,36 +107,42 @@
     }
 
     /* <------------- Tests Using VP9 -------------> */
+    @Test
     public void testVP9GLViewVideoDecode() throws Exception {
         runVP9DecodeAccuracyTest(
                 new GLSurfaceViewFactory(),
                 new VideoFormat(VP9_VIDEO_FILE_NAME));
     }
 
+    @Test
     public void testVP9GLViewLargerHeightVideoDecode() throws Exception {
         runVP9DecodeAccuracyTest(
                 new GLSurfaceViewFactory(),
                 getLargerHeightVideoFormat(new VideoFormat(VP9_VIDEO_FILE_NAME)));
     }
 
+    @Test
     public void testVP9GLViewLargerWidthVideoDecode() throws Exception {
         runVP9DecodeAccuracyTest(
                 new GLSurfaceViewFactory(),
                 getLargerWidthVideoFormat(new VideoFormat(VP9_VIDEO_FILE_NAME)));
     }
 
+    @Test
     public void testVP9SurfaceViewVideoDecode() throws Exception {
         runVP9DecodeAccuracyTest(
                 new SurfaceViewFactory(),
                 new VideoFormat(VP9_VIDEO_FILE_NAME));
     }
 
+    @Test
     public void testVP9SurfaceViewLargerHeightVideoDecode() throws Exception {
         runVP9DecodeAccuracyTest(
                 new SurfaceViewFactory(),
                 getLargerHeightVideoFormat(new VideoFormat(VP9_VIDEO_FILE_NAME)));
     }
 
+    @Test
     public void testVP9SurfaceViewLargerWidthVideoDecode() throws Exception {
         runVP9DecodeAccuracyTest(
                 new SurfaceViewFactory(),
@@ -122,12 +150,14 @@
     }
 
     /* <------------- Tests H264 with cropping -------------> */
+    @Test
     public void testH264GLViewCroppedVideoDecode() throws Exception {
         runH264DecodeCroppedTest(
                 new GLSurfaceViewFactory(),
                 new VideoFormat(H264_CROPPED_VIDEO_FILE_NAME));
     }
 
+    @Test
     public void testH264SurfaceViewCroppedVideoDecode() throws Exception {
         runH264DecodeCroppedTest(
                 new SurfaceViewFactory(),
@@ -136,17 +166,23 @@
 
     private void runH264DecodeAccuracyTest(
             VideoViewFactory videoViewFactory, VideoFormat videoFormat) {
-        runDecodeAccuracyTest(videoViewFactory, videoFormat, R.raw.h264decodertestgolden);
+        if (MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) {
+            runDecodeAccuracyTest(videoViewFactory, videoFormat, R.raw.h264decodertestgolden);
+        }
     }
 
     private void runVP9DecodeAccuracyTest(
             VideoViewFactory videoViewFactory, VideoFormat videoFormat) {
-        runDecodeAccuracyTest(videoViewFactory, videoFormat, R.raw.vp9decodertestgolden);
+        if (MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_VP9)) {
+            runDecodeAccuracyTest(videoViewFactory, videoFormat, R.raw.vp9decodertestgolden);
+        }
     }
 
     private void runH264DecodeCroppedTest(
             VideoViewFactory videoViewFactory, VideoFormat videoFormat) {
-        runDecodeAccuracyTest(videoViewFactory, videoFormat, R.raw.h264decodertest520x360golden);
+        if (MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) {
+            runDecodeAccuracyTest(videoViewFactory, videoFormat, R.raw.h264decodertest520x360golden);
+        }
     }
 
     private void runDecodeAccuracyTest(
@@ -191,7 +227,8 @@
 
     private void validateResult(
             VideoFormat videoFormat, VideoViewSnapshot videoViewSnapshot, int goldenResId) {
-        final Bitmap result = getHelper().generateBitmapFromVideoViewSnapshot(videoViewSnapshot);
+        final Bitmap result = checkNotNull("The expected bitmap from snapshot is null",
+                getHelper().generateBitmapFromVideoViewSnapshot(videoViewSnapshot));
         final Bitmap golden = getHelper().generateBitmapFromImageResourceId(goldenResId);
         final BitmapCompare.Difference difference = BitmapCompare.computeMinimumDifference(
                 result, golden, videoFormat.getOriginalWidth(), videoFormat.getOriginalHeight());
diff --git a/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java b/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java
index fae1bb4..1ce732d 100644
--- a/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java
+++ b/tests/tests/media/src/android/media/cts/DecodeAccuracyTestBase.java
@@ -44,6 +44,8 @@
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
 import android.test.ActivityInstrumentationTestCase2;
 import android.util.Log;
 import android.util.Pair;
@@ -73,7 +75,12 @@
 import javax.microedition.khronos.egl.EGLDisplay;
 import javax.microedition.khronos.egl.EGLSurface;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
 @TargetApi(16)
+@RunWith(AndroidJUnit4.class)
 public class DecodeAccuracyTestBase
     extends ActivityInstrumentationTestCase2<DecodeAccuracyTestActivity> {
 
@@ -86,9 +93,12 @@
         super(DecodeAccuracyTestActivity.class);
     }
 
+    @Before
     @Override
-    protected void setUp() throws Exception {
+    public void setUp() throws Exception {
         super.setUp();
+        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
+        setActivityInitialTouchMode(false);
         mActivity = getActivity();
         getInstrumentation().waitForIdleSync();
         mContext = getInstrumentation().getTargetContext();
@@ -96,8 +106,9 @@
         testHelper = new TestHelper(mContext, mActivity);
     }
 
+    @After
     @Override
-    protected void tearDown() throws Exception {
+    public void tearDown() throws Exception {
         mActivity = null;
         super.tearDown();
     }
@@ -117,6 +128,11 @@
         return reference;
     }
 
+    public static <T> T checkNotNull(String msg, T reference) {
+        assertNotNull(msg, reference);
+        return reference;
+    }
+
     public static class SimplePlayer {
 
         public static final long DECODE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(1) / 2;
@@ -419,6 +435,8 @@
     /* Utility class for collecting common test case functionality. */
     class TestHelper {
 
+        private final String TAG =  TestHelper.class.getSimpleName();
+
         private final Context context;
         private final Handler handler;
         private final Activity activity;
@@ -473,13 +491,21 @@
         }
 
         public synchronized Bitmap generateBitmapFromVideoViewSnapshot(VideoViewSnapshot snapshot) {
+            final long timeOutMs = TimeUnit.SECONDS.toMillis(10);
+            final long start = SystemClock.elapsedRealtime();
             handler.post(snapshot);
             try {
-                while (!snapshot.isBitmapReady()) {
+                while (!snapshot.isBitmapReady()
+                        && (SystemClock.elapsedRealtime() - start < timeOutMs)) {
                     Thread.sleep(100);
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
+                return null;
+            }
+            if (!snapshot.isBitmapReady()) {
+                Log.e(TAG, "Time out in generateBitmapFromVideoViewSnapshot().");
+                return null;
             }
             return snapshot.getBitmap();
         }
@@ -1165,8 +1191,7 @@
 class SurfaceViewSnapshot extends VideoViewSnapshot  {
 
     private static final String TAG = SurfaceViewSnapshot.class.getSimpleName();
-    private static final int PIXELCOPY_REQUEST_SLEEP_MS = 30;
-    private static final int PIXELCOPY_REQUEST_MAX_ATTEMPTS = 20;
+    private static final int PIXELCOPY_REQUEST_SLEEP_MS = 100;
     private static final int PIXELCOPY_TIMEOUT_MS = 1000;
 
     private final Thread copyThread;
@@ -1182,15 +1207,13 @@
                 bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
                 try {
                     // Wait for SurfaceView to be available.
-                    for (int i = 0; i < PIXELCOPY_REQUEST_MAX_ATTEMPTS; i++) {
-                        copyResult = copyHelper.request(surfaceView, bitmap);
-                        if (copyResult == PixelCopy.SUCCESS) {
-                            break;
-                        }
+                    while (copyResult != PixelCopy.SUCCESS) {
                         Thread.sleep(PIXELCOPY_REQUEST_SLEEP_MS);
+                        copyResult = copyHelper.request(surfaceView, bitmap);
                     }
                 } catch (InterruptedException e) {
-                    Log.w(TAG, "Pixel Copy is stopped/interrupted before it finishes.", e);
+                    Log.e(TAG, "Pixel Copy is stopped/interrupted before it finishes.", e);
+                    bitmap = null;
                 }
                 copyHelper.release();
             }
@@ -1294,10 +1317,10 @@
         try {
             waitForByteBuffer();
         } catch (InterruptedException e) {
-            Log.w(TAG, e.getMessage());
-            Log.w(TAG, "ByteBuffer may contain incorrect pixels.");
+            Log.e(TAG, e.getMessage());
+            bitmap = null;
+            return;
         }
-        // Get ByteBuffer anyway. Let the test fail if ByteBuffer contains incorrect pixels.
         ByteBuffer byteBuffer = glSurfaceViewFactory.getByteBuffer();
         bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
         byteBuffer.rewind();
diff --git a/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
index b8478d2..185ebfa 100644
--- a/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -387,6 +387,10 @@
      * Tests reporting of connectivity changed.
      */
     public void testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent() {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            Log.i(TAG, "testConnectivityChanged_manifestRequestOnly_shouldNotReceiveIntent cannot execute unless device supports WiFi");
+            return;
+        }
         ConnectivityReceiver.prepare();
 
         toggleWifi();
@@ -400,6 +404,10 @@
     }
 
     public void testConnectivityChanged_whenRegistered_shouldReceiveIntent() {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            Log.i(TAG, "testConnectivityChanged_whenRegistered_shouldReceiveIntent cannot execute unless device supports WiFi");
+            return;
+        }
         ConnectivityReceiver.prepare();
         ConnectivityReceiver receiver = new ConnectivityReceiver();
         IntentFilter filter = new IntentFilter();
@@ -416,6 +424,10 @@
 
     public void testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent()
             throws InterruptedException {
+        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+            Log.i(TAG, "testConnectivityChanged_manifestRequestOnlyPreN_shouldReceiveIntent cannot execute unless device supports WiFi");
+            return;
+        }
         Intent startIntent = new Intent();
         startIntent.setComponent(new ComponentName("android.net.cts.appForApi23",
                 "android.net.cts.appForApi23.ConnectivityListeningActivity"));
diff --git a/tests/tests/opengl/Android.mk b/tests/tests/opengl/Android.mk
index 69090f5..dff1778 100644
--- a/tests/tests/opengl/Android.mk
+++ b/tests/tests/opengl/Android.mk
@@ -33,7 +33,9 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_SDK_VERSION := current
+# 18 required for GLES 3, but going that low will stop some ABIs from
+# building. Using official Nougat level 24 to avoid missing something.
+LOCAL_SDK_VERSION := 24
 
 # Tag this module as a cts test artifact
 LOCAL_COMPATIBILITY_SUITE := cts
diff --git a/tests/tests/opengl/AndroidManifest.xml b/tests/tests/opengl/AndroidManifest.xml
index cc0ab8f..bb79490 100644
--- a/tests/tests/opengl/AndroidManifest.xml
+++ b/tests/tests/opengl/AndroidManifest.xml
@@ -19,7 +19,8 @@
     android:versionName="1.0" >
 
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
-    <uses-sdk android:minSdkVersion="14" />
+    <!-- GLES30 requires 18 -->
+    <uses-sdk android:minSdkVersion="18" />
     <uses-feature android:glEsVersion="0x00020000"/>
     <instrumentation
         android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityOne.java b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityOne.java
index 5acac32..33f097c 100644
--- a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityOne.java
+++ b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityOne.java
@@ -41,8 +41,11 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Window window = getWindow();
-        window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
-
+        window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_FULLSCREEN
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
         int viewType = getIntent().getIntExtra(EXTRA_VIEW_TYPE, -1);
         int viewIndex = getIntent().getIntExtra(EXTRA_VIEW_INDEX, -1);
 
diff --git a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityTwo.java b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityTwo.java
index 8ed0b9c..f0c7881 100644
--- a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityTwo.java
+++ b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20ActivityTwo.java
@@ -20,6 +20,8 @@
 import android.opengl.GLSurfaceView;
 import android.opengl.GLSurfaceView.Renderer;
 import android.os.Bundle;
+import android.view.Window;
+import android.view.WindowManager;
 
 import java.lang.InterruptedException;
 import java.util.concurrent.CountDownLatch;
@@ -48,14 +50,20 @@
     }
 
     public void setView(int type, int i, float[] vertexColors ) {
-        view = new OpenGLES20View(this,type,i, vertexColors, mLatch);
+        // Note: Flags should be modified before the content view is set
+        Window window = getWindow();
+        window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_FULLSCREEN
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        view = new OpenGLES20View(this, type, i, vertexColors, mLatch);
         setContentView(view);
     }
 
     public void setView(int type, int i) {
         float[] f = {};
-        view = new OpenGLES20View(this, type, i, f, mLatch)  ;
-        setContentView(view);
+        setView(type, i, f);
     }
 
     public int getNoOfAttachedShaders() {
diff --git a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityOne.java b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityOne.java
index 4602d4f..61f97b3 100644
--- a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityOne.java
+++ b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityOne.java
@@ -51,8 +51,11 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Window window = getWindow();
-        window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
-
+        window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_FULLSCREEN
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
         int viewType = getIntent().getIntExtra(EXTRA_VIEW_TYPE, -1);
         int viewIndex = getIntent().getIntExtra(EXTRA_VIEW_INDEX, -1);
 
diff --git a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityTwo.java b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityTwo.java
index 6bdf95f..90964f8 100644
--- a/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityTwo.java
+++ b/tests/tests/opengl/src/android/opengl/cts/OpenGLES20NativeActivityTwo.java
@@ -20,8 +20,11 @@
 import android.opengl.GLSurfaceView;
 import android.opengl.GLSurfaceView.Renderer;
 import android.os.Bundle;
+import android.view.Window;
+import android.view.WindowManager;
 
 import java.lang.InterruptedException;
+import java.lang.UnsupportedOperationException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -48,12 +51,19 @@
     }
 
     public void setView(int type, int i, float[] vertexColors ) {
+        // Note: Flags should be modified before the content view is set
+        Window window = getWindow();
+        window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_FULLSCREEN
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
         view = new OpenGLES20View(this,type,i, vertexColors, mLatch);
         setContentView(view);
     }
 
     public void setView(int type, int i) {
-
+        throw new UnsupportedOperationException("No views without vertexColors, please!");
     }
 
     public int getNoOfAttachedShaders() {
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/TextureTestActivity.java b/tests/tests/openglperf/src/android/openglperf/cts/TextureTestActivity.java
index 4d7d641..6459c31 100644
--- a/tests/tests/openglperf/src/android/openglperf/cts/TextureTestActivity.java
+++ b/tests/tests/openglperf/src/android/openglperf/cts/TextureTestActivity.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.opengl.GLSurfaceView;
 import android.os.Bundle;
+import android.view.WindowManager;
 
 public class TextureTestActivity extends Activity {
     private GLSurfaceView mGLView;
@@ -27,6 +28,11 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mGLView = new TextureTestSurfaceView(this);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_FULLSCREEN
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
         setContentView(mGLView);
     }
 
diff --git a/tests/tests/print/src/android/print/cts/CustomPrintOptionsTest.java b/tests/tests/print/src/android/print/cts/CustomPrintOptionsTest.java
index 54b4d23..c65118b 100644
--- a/tests/tests/print/src/android/print/cts/CustomPrintOptionsTest.java
+++ b/tests/tests/print/src/android/print/cts/CustomPrintOptionsTest.java
@@ -378,7 +378,9 @@
         }
 
         // Abort printing
-        getActivity().finish();
+        getUiDevice().pressBack();
+        getUiDevice().pressBack();
+        getUiDevice().pressBack();
 
         waitForPrinterDiscoverySessionDestroyCallbackCalled(1);
     }
diff --git a/tests/tests/security/src/android/security/cts/EncryptionTest.java b/tests/tests/security/src/android/security/cts/EncryptionTest.java
index 1d37ec6..85d82e2 100644
--- a/tests/tests/security/src/android/security/cts/EncryptionTest.java
+++ b/tests/tests/security/src/android/security/cts/EncryptionTest.java
@@ -16,12 +16,13 @@
 
 package android.security.cts;
 
+import com.android.compatibility.common.util.PropertyUtil;
+
 import android.test.AndroidTestCase;
 import junit.framework.TestCase;
 
 import android.app.ActivityManager;
 import android.content.Context;
-import android.os.SystemProperties;
 import android.util.Log;
 import java.io.BufferedReader;
 import java.io.FileReader;
@@ -34,7 +35,7 @@
         System.loadLibrary("ctssecurity_jni");
     }
 
-    private static final int min_api_level = 23;
+    private static final int MIN_API_LEVEL = 23;
 
     private static final String TAG = "EncryptionTest";
 
@@ -77,15 +78,8 @@
     }
 
     private boolean isRequired() {
-        int first_api_level =
-            SystemProperties.getInt("ro.product.first_api_level", 0);
-
-        // Optional before min_api_level or if the device has low RAM
-        if (first_api_level > 0 && first_api_level < min_api_level) {
-            return false;
-        } else {
-            return !hasLowRAM();
-        }
+        // Optional before MIN_API_LEVEL or if the device has low RAM
+        return PropertyUtil.getFirstApiLevel() >= MIN_API_LEVEL && !hasLowRAM();
     }
 
     public void testConfig() throws Exception {
diff --git a/tests/tests/security/src/android/security/cts/StagefrightTest.java b/tests/tests/security/src/android/security/cts/StagefrightTest.java
index 647d35b..ef3dde5 100644
--- a/tests/tests/security/src/android/security/cts/StagefrightTest.java
+++ b/tests/tests/security/src/android/security/cts/StagefrightTest.java
@@ -319,6 +319,7 @@
         assertFalse("Device *IS* vulnerable to " + cve,
                     mpcl.waitForError() == MediaPlayer.MEDIA_ERROR_SERVER_DIED);
         t.stopLooper();
+        t.join(); // wait for thread to exit so we're sure the player was released
     }
 
     private void doStagefrightTestMediaCodec(final int rid) throws Exception {
diff --git a/tests/tests/widget/src/android/widget/cts/ListPopupWindowTest.java b/tests/tests/widget/src/android/widget/cts/ListPopupWindowTest.java
index b50f8c9..b882c0c 100644
--- a/tests/tests/widget/src/android/widget/cts/ListPopupWindowTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ListPopupWindowTest.java
@@ -508,7 +508,10 @@
         final int[] lastChildOnScreenXY = new int[2];
         lastListChild.getLocationOnScreen(lastChildOnScreenXY);
 
-        assertTrue(lastChildOnScreenXY[1] + lastListChild.getHeight() <= promptViewOnScreenXY[1]);
+        // The child is above the prompt. They may overlap, as in the case
+        // when the list items do not all fit on screen, but this is still
+        // correct.
+        assertTrue(lastChildOnScreenXY[1] <= promptViewOnScreenXY[1]);
     }
 
     @Presubmit