Move dashboard to test/vti.

Copy code from test/vts/web to test/vti/.

Bug: 62339915
Test: none
Change-Id: I6e07fbaca77018f6d7bd24bd3da9e3bbbafab495
(cherry picked from commit 67c9a5397bd972a173cdf08aaccba44678f3aea0)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9a94e36
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,45 @@
+# Google App Engine generated folder
+appengine-generated/
+
+# Java
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+
+service-account.json
+
+# intellij
+.idea/
+*.iml
+
+# Eclipse files
+.project
+.classpath
+.settings
+
+# vim
+[._]*.s[a-w][a-z]
+[._]s[a-w][a-z]
+Session.vim
+.netrwhist
+*~
+tags
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9b6ca36
--- /dev/null
+++ b/README.md
@@ -0,0 +1,151 @@
+# VTS Dashboard
+
+## Introduction
+
+The VTS Dashboard displays the summarized results of the Multi Device Tests along with graphs.
+
+## Installation
+
+### Steps to run locally:
+
+1. Google App Engine uses Java 8. Install Java 8 before running running locally:
+   'sudo apt install openjdk-8-jdk'
+
+   To use java 8:
+   Copy the following lines in ~/.bashrc :
+
+```
+    function setup_jdk() {
+      # Remove the current JDK from PATH
+      if [ -n "$JAVA_HOME" ] ; then
+        PATH=${PATH/$JAVA_HOME\/bin:/}
+      fi
+      export JAVA_HOME=$1
+      export PATH=$JAVA_HOME/bin:$PATH
+    }
+
+    function use_java8() {
+    #  setup_jdk /usr/java/jre1.8.0_73
+      setup_jdk /usr/lib/jvm/java-8-openjdk-amd64
+    }
+
+    Then from cmd:
+    $ use_java8
+```
+
+2. Maven is used for build. Install Maven 3.3.9:
+   Download maven from:
+   https://maven.apache.org/download.cgi
+
+   Steps to Install Maven:
+   1) Unzip the Binary tar:
+      tar -zxf apache-maven-3.3.3-bin.tar.gz
+
+   2) Move the application directory to /usr/local
+      sudo cp -R apache-maven-3.3.3 /usr/local
+
+   3) Make a soft link in /usr/bin for universal access of mvn
+      sudo ln -s /usr/local/apache-maven-3.3.3/bin/mvn /usr/bin/mvn
+
+   4) Verify maven installation:
+      $ mvn -v
+
+      The output should resemble this:
+
+      Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-10T08:41:47-08:00)
+      Maven home: /opt/apache-maven-3.3.9
+      Java version: 1.8.0_45-internal, vendor: Oracle Corporation
+      Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre
+      Default locale: en_US, platform encoding: UTF-8
+      OS name: "linux", version: "3.13.0-88-generic", arch: "amd64", family: "unix"
+
+3. Install Google Cloud SDK. Follow the instructions listed on official source:
+   https://cloud.google.com/sdk/docs/quickstart-linux
+
+   The default location where the application searches for a google-cloud-sdk is:
+   /usr/local/share/google/google-cloud-sdk
+
+   Therefore move the extracted folder to this location: /usr/local/share/google/
+
+   Otherwise, to have a custom location, specify the location of
+   google-cloud-sdk in test/vti/dashboard/pom.xml by putting the configuration:
+
+```
+   <configuration>
+     <gcloud_directory>PATH/TO/GCLOUD_DIRECTORY</gcloud_directory>
+   </configuration>
+```
+   within the 'com.google.appengine' plugin tag :
+
+## To run GAE on local machine:
+
+$ cd test/vti/dashboard
+$ mvn appengine:devserver
+
+## To deploy to Google App Engine
+
+$ cd test/vti/dashboard
+$ mvn appengine:update
+
+visit https://<YOUR-PROJECT-NAME>.appspot.com
+
+## Monitoring
+
+The following steps list how to create a monitoring service for the VTS Dashboard.
+
+### Create a Stackdriver account
+
+1. Go to Google Cloud Platform Console:
+   http://console.developers.google.com
+
+2. In the Google Cloud Platform Console, select Stackdriver > Monitoring.
+   If your project is not in a Stackdriver account you'll see a message to
+   create a new project.
+
+3. Click Create new Stackdriver account and then Continue.
+
+4. With your project shown, click Create account.
+
+5. In the page, "Add Google Cloud Platform projects to monitor", click Continue to skip ahead.
+
+6. In the page, "Monitor AWS accounts", click Done to skip ahead.
+
+7. In a few seconds you see the following message:
+   "Finished Initial collection"
+   Click Launch Monitoring.
+
+8. In the page, "Get reports by email", click No reports and Continue.
+
+9. You will see your Stackdriver account dashboard.
+   Close the "Welcome to Stackdriver" banner if you don't need it.
+
+### Steps to create an uptime check and an alerting policy
+
+1. Go to Stack Monitoring console:
+   https://app.google.stackdriver.com/
+
+2. Go to Alerting > Uptime Checks in the top menu and then click Add Uptime Check.
+   You see the New Uptime Check panel.
+
+3. Fill in the following fields for the uptime check:
+
+    Check type: HTTP
+    Resource Type: Instance
+    Applies To: Single, lamp-1-vm
+    Leave the other fields with their default values.
+
+4. Click Test to verify your uptime check is working.
+
+5. Click Save. After you click on save you'll see a panel to
+   'Create Alerting Policy'
+
+6. Fill out the configuration for notifications and click save policy.
+
+### Test the check and alert
+
+This procedure can take up to fifteen minutes.
+
+To test the check and alert, go to the VM Instances page, select your instance, and click Stop from the top menu.
+You'll have to wait up to five minutes for the next uptime check to fail. The alert and notification don't happen until the next failure occurs.
+
+To correct the "problem," return to the VM Instances page, select your instance, and click Start from the top menu.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..d5bc0ff
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,187 @@
+<!--
+  Copyright 2016 Google Inc.
+
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <packaging>war</packaging>
+  <version>1.0-SNAPSHOT</version>
+  <groupId>com.android.vts</groupId>
+  <artifactId>vts-dashboard</artifactId>
+
+  <properties>
+    <appengine.clientID></appengine.clientID>
+    <appengine.serviceClientID></appengine.serviceClientID>
+    <appengine.senderEmail></appengine.senderEmail>
+    <appengine.emailDomain></appengine.emailDomain>
+    <appengine.defaultEmail></appengine.defaultEmail>
+    <gerrit.uri></gerrit.uri>
+    <gerrit.scope></gerrit.scope>
+    <analytics.id></analytics.id>
+
+    <maven.compiler.target>1.7</maven.compiler.target>
+    <maven.compiler.source>1.7</maven.compiler.source>
+    <maven.war.filteringDeploymentDescriptors>true</maven.war.filteringDeploymentDescriptors>
+
+    <failOnMissingWebXml>false</failOnMissingWebXml>
+  </properties>
+
+
+  <dependencies>
+
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <version>2.5</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>jsp-api</artifactId>
+      <version>2.0</version>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-lang</groupId>
+      <artifactId>commons-lang</artifactId>
+      <version>2.6</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-math3</artifactId>
+      <version>3.6.1</version>
+    </dependency>
+
+    <dependency>
+        <groupId>commons-codec</groupId>
+        <artifactId>commons-codec</artifactId>
+        <version>1.9</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud</artifactId>
+      <version>0.8.0</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.7</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.8.6</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.api-client</groupId>
+      <artifactId>google-api-client</artifactId>
+      <version>1.22.0</version>
+      <exclusions>
+        <exclusion>
+          <groupId>com.google.guava</groupId>
+          <artifactId>guava-jdk5</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-api-1.0-sdk</artifactId>
+      <version>1.9.50</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.apis</groupId>
+      <artifactId>google-api-services-oauth2</artifactId>
+      <version>v1-rev131-1.22.0</version>
+    </dependency>
+
+    <!-- Test Dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.appengine</groupId>
+      <artifactId>appengine-testing</artifactId>
+      <version>1.9.50</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+        <groupId>com.google.appengine</groupId>
+        <artifactId>appengine-api-stubs</artifactId>
+        <version>1.9.50</version>
+        <scope>test</scope>
+    </dependency>
+    <dependency>
+        <groupId>com.google.appengine</groupId>
+        <artifactId>appengine-tools-sdk</artifactId>
+        <version>1.9.50</version>
+        <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+
+
+  <build>
+    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/classes</outputDirectory>
+
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <version>2.6</version>
+        <configuration>
+          <archiveClasses>true</archiveClasses>
+          <failOnMissingWebXml>false</failOnMissingWebXml>
+          <webResources>
+            <resource>
+              <directory>${basedir}/src/main/webapp/WEB-INF</directory>
+              <filtering>true</filtering>
+              <targetPath>WEB-INF</targetPath>
+            </resource>
+          </webResources>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <version>3.3</version>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <source>1.7</source>
+          <target>1.7</target>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>com.google.appengine</groupId>
+        <artifactId>appengine-maven-plugin</artifactId>
+        <version>1.9.51</version>
+      </plugin>
+
+    </plugins>
+  </build>
+</project>
diff --git a/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java b/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java
new file mode 100644
index 0000000..d2b4a61
--- /dev/null
+++ b/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.api;
+
+import com.android.vts.proto.VtsReportMessage.TestReportMessage;
+import com.android.vts.util.DatastoreHelper;
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson.JacksonFactory;
+import com.google.api.services.oauth2.Oauth2;
+import com.google.api.services.oauth2.model.Tokeninfo;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.codec.binary.Base64;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** REST endpoint for posting data JSON to the Dashboard. */
+@Deprecated
+public class BigtableLegacyJsonServlet extends HttpServlet {
+    private static final String SERVICE_CLIENT_ID = System.getProperty("SERVICE_CLIENT_ID");
+    private static final String SERVICE_NAME = "VTS Dashboard";
+    private static final Logger logger =
+            Logger.getLogger(BigtableLegacyJsonServlet.class.getName());
+
+    @Override
+    public void doPost(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        // Retrieve the params
+        String payload = new String();
+        JSONObject payloadJson;
+        try {
+            String line = null;
+            BufferedReader reader = request.getReader();
+            while ((line = reader.readLine()) != null) {
+                payload += line;
+            }
+            payloadJson = new JSONObject(payload);
+        } catch (IOException | JSONException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            logger.log(Level.WARNING, "Invalid JSON: " + payload);
+            return;
+        }
+
+        // Verify service account access token.
+        boolean authorized = false;
+        if (payloadJson.has("accessToken")) {
+            String accessToken = payloadJson.getString("accessToken").trim();
+            GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
+            Oauth2 oauth2 =
+                    new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
+                            .setApplicationName(SERVICE_NAME)
+                            .build();
+            Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(accessToken).execute();
+            if (tokenInfo.getIssuedTo().equals(SERVICE_CLIENT_ID)) {
+                authorized = true;
+            }
+        }
+
+        if (!authorized) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+
+        // Parse the desired action and execute the command
+        try {
+            if (payloadJson.has("verb")) {
+                switch (payloadJson.getString("verb")) {
+                    case "createTable":
+                        logger.log(Level.INFO, "Deprecated verb: createTable.");
+                        break;
+                    case "insertRow":
+                        insertData(payloadJson);
+                        break;
+                    default:
+                        logger.log(Level.WARNING,
+                                "Invalid Datastore REST verb: " + payloadJson.getString("verb"));
+                        throw new IOException("Unsupported POST verb.");
+                }
+            }
+        } catch (IOException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        response.setStatus(HttpServletResponse.SC_OK);
+    }
+
+    /**
+     * Inserts a data into the Cloud Datastore
+     *
+     * @param payloadJson The JSON object representing the row to be inserted. Of the form: {
+     *     (deprecated) 'tableName' : 'table', (deprecated) 'rowKey' : 'row', (deprecated) 'family'
+     * :
+     *     'family', (deprecated) 'qualifier' : 'qualifier', 'value' : 'value' }
+     * @throws IOException
+     */
+    private void insertData(JSONObject payloadJson) throws IOException {
+        if (!payloadJson.has("value")) {
+            logger.log(Level.WARNING, "Missing attributes for datastore api insertRow().");
+            return;
+        }
+        try {
+            byte[] value = Base64.decodeBase64(payloadJson.getString("value"));
+            TestReportMessage testReportMessage = TestReportMessage.parseFrom(value);
+            DatastoreHelper.insertTestReport(testReportMessage);
+        } catch (InvalidProtocolBufferException e) {
+            logger.log(Level.WARNING, "Invalid report posted to dashboard.");
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/api/DatastoreRestServlet.java b/src/main/java/com/android/vts/api/DatastoreRestServlet.java
new file mode 100644
index 0000000..761c292
--- /dev/null
+++ b/src/main/java/com/android/vts/api/DatastoreRestServlet.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.api;
+
+import com.android.vts.proto.VtsReportMessage.DashboardPostMessage;
+import com.android.vts.proto.VtsReportMessage.TestPlanReportMessage;
+import com.android.vts.proto.VtsReportMessage.TestReportMessage;
+import com.android.vts.util.DatastoreHelper;
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson.JacksonFactory;
+import com.google.api.services.oauth2.Oauth2;
+import com.google.api.services.oauth2.model.Tokeninfo;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.codec.binary.Base64;
+
+/** REST endpoint for posting data to the Dashboard. */
+public class DatastoreRestServlet extends HttpServlet {
+    private static final String SERVICE_CLIENT_ID = System.getProperty("SERVICE_CLIENT_ID");
+    private static final String SERVICE_NAME = "VTS Dashboard";
+    private static final Logger logger = Logger.getLogger(DatastoreRestServlet.class.getName());
+
+    @Override
+    public void doPost(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        // Retrieve the params
+        String payload = new String();
+        DashboardPostMessage postMessage;
+        try {
+            String line = null;
+            BufferedReader reader = request.getReader();
+            while ((line = reader.readLine()) != null) {
+                payload += line;
+            }
+            byte[] value = Base64.decodeBase64(payload);
+            postMessage = DashboardPostMessage.parseFrom(value);
+        } catch (IOException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            logger.log(Level.WARNING, "Invalid proto: " + payload);
+            return;
+        }
+
+        // Verify service account access token.
+        boolean authorized = false;
+        if (postMessage.hasAccessToken()) {
+            String accessToken = postMessage.getAccessToken();
+            GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
+            Oauth2 oauth2 =
+                    new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
+                            .setApplicationName(SERVICE_NAME)
+                            .build();
+            Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(accessToken).execute();
+            if (tokenInfo.getIssuedTo().equals(SERVICE_CLIENT_ID)) {
+                authorized = true;
+            }
+        }
+
+        if (!authorized) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return;
+        }
+
+        for (TestReportMessage testReportMessage : postMessage.getTestReportList()) {
+            DatastoreHelper.insertTestReport(testReportMessage);
+        }
+
+        for (TestPlanReportMessage planReportMessage : postMessage.getTestPlanReportList()) {
+            DatastoreHelper.insertTestPlanReport(planReportMessage);
+        }
+
+        response.setStatus(HttpServletResponse.SC_OK);
+    }
+}
diff --git a/src/main/java/com/android/vts/api/TestRunRestServlet.java b/src/main/java/com/android/vts/api/TestRunRestServlet.java
new file mode 100644
index 0000000..ac84a3b
--- /dev/null
+++ b/src/main/java/com/android/vts/api/TestRunRestServlet.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.api;
+
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.util.TestRunDetails;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Servlet for handling requests to fetch test case results. */
+public class TestRunRestServlet extends HttpServlet {
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        String test = request.getParameter("test");
+        String timeString = request.getParameter("timestamp");
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+        long timestamp;
+        try {
+            timestamp = Long.parseLong(timeString);
+            if (timestamp <= 0)
+                throw new NumberFormatException();
+            timestamp = timestamp > 0 ? timestamp : null;
+        } catch (NumberFormatException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+
+        Key testKey = KeyFactory.createKey(TestEntity.KIND, test);
+        Key testRunKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, timestamp);
+        TestRunEntity testRunEntity;
+        try {
+            Entity testRun = datastore.get(testRunKey);
+            testRunEntity = TestRunEntity.fromEntity(testRun);
+            if (testRunEntity == null) {
+                throw new EntityNotFoundException(testRunKey);
+            }
+        } catch (EntityNotFoundException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        TestRunDetails details = new TestRunDetails();
+        List<Key> gets = new ArrayList<>();
+        for (long testCaseId : testRunEntity.testCaseIds) {
+            gets.add(KeyFactory.createKey(TestCaseRunEntity.KIND, testCaseId));
+        }
+        Map<Key, Entity> entityMap = datastore.get(gets);
+        for (Key key : entityMap.keySet()) {
+            TestCaseRunEntity testCaseRun = TestCaseRunEntity.fromEntity(entityMap.get(key));
+            if (testCaseRun == null) {
+                continue;
+            }
+            details.addTestCase(testCaseRun);
+        }
+        response.setContentType("application/json");
+        PrintWriter writer = response.getWriter();
+        writer.print(new Gson().toJson(details.toJson()));
+        writer.flush();
+    }
+}
diff --git a/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java b/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
new file mode 100644
index 0000000..f863051
--- /dev/null
+++ b/src/main/java/com/android/vts/api/UserFavoriteRestServlet.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.api;
+
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.UserFavoriteEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Transaction;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Servlet for handling requests to add or remove subscriptions. */
+public class UserFavoriteRestServlet extends HttpServlet {
+    protected static final Logger logger =
+            Logger.getLogger(UserFavoriteRestServlet.class.getName());
+
+    /**
+     * Add a test to the user's favorites.
+     */
+    @Override
+    public void doPost(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        UserService userService = UserServiceFactory.getUserService();
+        User currentUser = userService.getCurrentUser();
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+        // Retrieve the added tests from the request.
+        String test = request.getPathInfo();
+        if (test == null) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        if (test.startsWith("/")) {
+            test = test.substring(1);
+        }
+        Key addedTestKey = KeyFactory.createKey(TestEntity.KIND, test);
+        // Filter the tests that exist from the set of tests to add
+        try {
+            datastore.get(addedTestKey);
+        } catch (EntityNotFoundException e) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+
+        Filter userFilter =
+                new FilterPredicate(UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
+        Filter testFilter = new FilterPredicate(
+                UserFavoriteEntity.TEST_KEY, FilterOperator.EQUAL, addedTestKey);
+        Query q = new Query(UserFavoriteEntity.KIND)
+                          .setFilter(CompositeFilterOperator.and(userFilter, testFilter))
+                          .setKeysOnly();
+
+        Key favoriteKey = null;
+
+        Transaction txn = datastore.beginTransaction();
+        try {
+            for (Entity e : datastore.prepare(q).asIterable()) {
+                favoriteKey = e.getKey();
+                break;
+            }
+            if (favoriteKey == null) {
+                UserFavoriteEntity favorite = new UserFavoriteEntity(currentUser, addedTestKey);
+                Entity entity = favorite.toEntity();
+                datastore.put(entity);
+                favoriteKey = entity.getKey();
+            }
+            txn.commit();
+        } finally {
+            if (txn.isActive()) {
+                logger.log(Level.WARNING,
+                        "Transaction rollback forced for favorite creation: " + test);
+                txn.rollback();
+            }
+        }
+
+        response.setContentType("application/json");
+        PrintWriter writer = response.getWriter();
+        JsonObject json = new JsonObject();
+        json.add("key", new JsonPrimitive(KeyFactory.keyToString(favoriteKey)));
+        writer.print(new Gson().toJson(json));
+        writer.flush();
+        response.setStatus(HttpServletResponse.SC_OK);
+    }
+
+    /**
+     * Remove a test from the user's favorites.
+     */
+    @Override
+    public void doDelete(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        String stringKey = request.getPathInfo();
+        if (stringKey == null) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        if (stringKey.startsWith("/")) {
+            stringKey = stringKey.substring(1);
+        }
+        datastore.delete(KeyFactory.stringToKey(stringKey));
+        response.setStatus(HttpServletResponse.SC_OK);
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/CoverageEntity.java b/src/main/java/com/android/vts/entity/CoverageEntity.java
new file mode 100644
index 0000000..a0f8cca
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/CoverageEntity.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.android.vts.proto.VtsReportMessage.CoverageReportMessage;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Object describing coverage data gathered for a file. */
+public class CoverageEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(CoverageEntity.class.getName());
+
+    public static final String KIND = "Coverage";
+
+    // Property keys
+    public static final String GROUP = "group";
+    public static final String COVERED_LINE_COUNT = "coveredCount";
+    public static final String TOTAL_LINE_COUNT = "totalCount";
+    public static final String FILE_PATH = "filePath";
+    public static final String PROJECT_NAME = "projectName";
+    public static final String PROJECT_VERSION = "projectVersion";
+    public static final String LINE_COVERAGE = "lineCoverage";
+
+    private final Key parentKey;
+
+    public final String group;
+    public final long coveredLineCount;
+    public final long totalLineCount;
+    public final String filePath;
+    public final String projectName;
+    public final String projectVersion;
+    public final List<Long> lineCoverage;
+
+    /**
+     * Create a CoverageEntity object for a file.
+     *
+     * @param parentKey The key to the parent TestRunEntity object in the database.
+     * @param group The group within the test run describing the coverage.
+     * @param coveredLineCount The total number of covered lines in the file.
+     * @param totalLineCount The total number of uncovered executable lines in the file.
+     * @param filePath The path to the file.
+     * @param projectName The name of the git project.
+     * @param projectVersion The commit hash of the project at the time the test was executed.
+     * @param lineCoverage List of coverage counts per executable line in the file.
+     */
+    public CoverageEntity(Key parentKey, String group, long coveredLineCount, long totalLineCount,
+            String filePath, String projectName, String projectVersion, List<Long> lineCoverage) {
+        this.parentKey = parentKey;
+        this.group = group;
+        this.coveredLineCount = coveredLineCount;
+        this.totalLineCount = totalLineCount;
+        this.filePath = filePath;
+        this.projectName = projectName;
+        this.projectVersion = projectVersion;
+        this.lineCoverage = lineCoverage;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity coverageEntity = new Entity(KIND, parentKey);
+        coverageEntity.setProperty(GROUP, group);
+        coverageEntity.setUnindexedProperty(COVERED_LINE_COUNT, coveredLineCount);
+        coverageEntity.setUnindexedProperty(TOTAL_LINE_COUNT, totalLineCount);
+        coverageEntity.setProperty(FILE_PATH, filePath);
+        coverageEntity.setUnindexedProperty(PROJECT_NAME, projectName);
+        coverageEntity.setUnindexedProperty(PROJECT_VERSION, projectVersion);
+        if (lineCoverage != null && lineCoverage.size() > 0) {
+            coverageEntity.setUnindexedProperty(LINE_COVERAGE, lineCoverage);
+        }
+        return coverageEntity;
+    }
+
+    /**
+     * Convert an Entity object to a CoverageEntity.
+     *
+     * @param e The entity to process.
+     * @return CoverageEntity object with the properties from e, or null if incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static CoverageEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || !e.hasProperty(GROUP) || !e.hasProperty(COVERED_LINE_COUNT)
+                || !e.hasProperty(TOTAL_LINE_COUNT) || !e.hasProperty(FILE_PATH)
+                || !e.hasProperty(PROJECT_NAME) || !e.hasProperty(PROJECT_VERSION)) {
+            logger.log(Level.WARNING, "Missing coverage attributes in entity: " + e.toString());
+            return null;
+        }
+        try {
+            String group = (String) e.getProperty(GROUP);
+            long coveredLineCount = (long) e.getProperty(COVERED_LINE_COUNT);
+            long totalLineCount = (long) e.getProperty(TOTAL_LINE_COUNT);
+            String filePath = (String) e.getProperty(FILE_PATH);
+            String projectName = (String) e.getProperty(PROJECT_NAME);
+            String projectVersion = (String) e.getProperty(PROJECT_VERSION);
+            List<Long> lineCoverage;
+            if (e.hasProperty(LINE_COVERAGE)) {
+                lineCoverage = (List<Long>) e.getProperty(LINE_COVERAGE);
+            } else {
+                lineCoverage = new ArrayList<>();
+            }
+            return new CoverageEntity(e.getKey().getParent(), group, coveredLineCount,
+                    totalLineCount, filePath, projectName, projectVersion, lineCoverage);
+        } catch (ClassCastException exception) {
+            // Invalid contents or null values
+            logger.log(Level.WARNING, "Error parsing coverage entity.", exception);
+        }
+        return null;
+    }
+
+    /**
+     * Convert a coverage report to a CoverageEntity.
+     *
+     * @param parentKey The ancestor key for the coverage entity.
+     * @param group The group to display the coverage report with.
+     * @param coverage The coverage report containing coverage data.
+     * @return The CoverageEntity for the coverage report message, or null if not compatible.
+     */
+    public static CoverageEntity fromCoverageReport(
+            Key parentKey, String group, CoverageReportMessage coverage) {
+        if (!coverage.hasFilePath() || !coverage.hasProjectName() || !coverage.hasRevision()
+                || !coverage.hasTotalLineCount() || !coverage.hasCoveredLineCount()) {
+            return null; // invalid coverage report;
+        }
+        long coveredLineCount = coverage.getCoveredLineCount();
+        long totalLineCount = coverage.getTotalLineCount();
+        String filePath = coverage.getFilePath().toStringUtf8();
+        String projectName = coverage.getProjectName().toStringUtf8();
+        String projectVersion = coverage.getRevision().toStringUtf8();
+        List<Long> lineCoverage = null;
+        if (coverage.getLineCoverageVectorCount() > 0) {
+            lineCoverage = new ArrayList<>();
+            for (int count : coverage.getLineCoverageVectorList()) {
+                lineCoverage.add((long) count);
+            }
+        }
+        return new CoverageEntity(parentKey, group, coveredLineCount, totalLineCount, filePath,
+                projectName, projectVersion, lineCoverage);
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/DashboardEntity.java b/src/main/java/com/android/vts/entity/DashboardEntity.java
new file mode 100644
index 0000000..402a1e5
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/DashboardEntity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.google.appengine.api.datastore.Entity;
+
+/** Interface for interacting with VTS Dashboard entities in Cloud Datastore. */
+public interface DashboardEntity {
+    /**
+     * Serialize the DashboardEntity to an Entity object.
+     *
+     * @return Entity object representing the properties defined in the DashboardEntity.
+     */
+    public Entity toEntity();
+}
diff --git a/src/main/java/com/android/vts/entity/DeviceInfoEntity.java b/src/main/java/com/android/vts/entity/DeviceInfoEntity.java
new file mode 100644
index 0000000..ad4e83b
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/DeviceInfoEntity.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Class describing a device used for a test run. */
+public class DeviceInfoEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(DeviceInfoEntity.class.getName());
+
+    public static final String KIND = "DeviceInfo";
+
+    // Property keys
+    public static final String BRANCH = "branch";
+    public static final String PRODUCT = "product";
+    public static final String BUILD_FLAVOR = "buildFlavor";
+    public static final String BUILD_ID = "buildId";
+    public static final String ABI_BITNESS = "abiBitness";
+    public static final String ABI_NAME = "abiName";
+
+    private final Key parentKey;
+
+    public final String branch;
+    public final String product;
+    public final String buildFlavor;
+    public final String buildId;
+    public final String abiBitness;
+    public final String abiName;
+
+    /**
+     * Create a DeviceInfoEntity object.
+     *
+     * @param parentKey The key for the parent entity in the database.
+     * @param branch The build branch.
+     * @param product The device product.
+     * @param buildFlavor The device build flavor.
+     * @param buildID The device build ID.
+     * @param abiBitness The abi bitness of the device.
+     * @param abiName The name of the abi.
+     */
+    public DeviceInfoEntity(Key parentKey, String branch, String product, String buildFlavor,
+            String buildID, String abiBitness, String abiName) {
+        this.parentKey = parentKey;
+        this.branch = branch;
+        this.product = product;
+        this.buildFlavor = buildFlavor;
+        this.buildId = buildID;
+        this.abiBitness = abiBitness;
+        this.abiName = abiName;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity deviceEntity = new Entity(KIND, this.parentKey);
+        deviceEntity.setProperty(BRANCH, this.branch.toLowerCase());
+        deviceEntity.setProperty(PRODUCT, this.product.toLowerCase());
+        deviceEntity.setProperty(BUILD_FLAVOR, this.buildFlavor.toLowerCase());
+        deviceEntity.setProperty(BUILD_ID, this.buildId.toLowerCase());
+        if (this.abiBitness != null && this.abiName != null) {
+            deviceEntity.setUnindexedProperty(ABI_BITNESS, this.abiBitness.toLowerCase());
+            deviceEntity.setUnindexedProperty(ABI_NAME, this.abiName.toLowerCase());
+        }
+
+        return deviceEntity;
+    }
+
+    /**
+     * Convert an Entity object to a DeviceInfoEntity.
+     *
+     * @param e The entity to process.
+     * @return DeviceInfoEntity object with the properties from e, or null if incompatible.
+     */
+    public static DeviceInfoEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || !e.hasProperty(BRANCH) || !e.hasProperty(PRODUCT)
+                || !e.hasProperty(BUILD_FLAVOR) || !e.hasProperty(BUILD_ID)
+                || !e.hasProperty(ABI_BITNESS) || !e.hasProperty(ABI_NAME)) {
+            logger.log(Level.WARNING, "Missing device info attributes in entity: " + e.toString());
+            return null;
+        }
+        try {
+            Key parentKey = e.getKey().getParent();
+            String branch = (String) e.getProperty(BRANCH);
+            String product = (String) e.getProperty(PRODUCT);
+            String buildFlavor = (String) e.getProperty(BUILD_FLAVOR);
+            String buildId = (String) e.getProperty(BUILD_ID);
+            String abiBitness = null;
+            String abiName = null;
+            if (e.hasProperty(ABI_BITNESS) && e.hasProperty(ABI_NAME)) {
+                abiBitness = (String) e.getProperty(ABI_BITNESS);
+                abiName = (String) e.getProperty(ABI_NAME);
+            }
+            return new DeviceInfoEntity(
+                    parentKey, branch, product, buildFlavor, buildId, abiBitness, abiName);
+        } catch (ClassCastException exception) {
+            // Invalid cast
+            logger.log(Level.WARNING, "Error parsing device info entity.", exception);
+        }
+        return null;
+    }
+
+    /**
+     * Convert a device info message to a DeviceInfoEntity.
+     *
+     * @param parentKey The ancestor key for the device entity.
+     * @param device The device info report describing the target Android device.
+     * @return The DeviceInfoEntity for the target device, or null if incompatible
+     */
+    public static DeviceInfoEntity fromDeviceInfoMessage(
+            Key parentKey, AndroidDeviceInfoMessage device) {
+        if (!device.hasBuildAlias() || !device.hasBuildFlavor() || !device.hasProductVariant()
+                || !device.hasBuildId()) {
+            return null;
+        }
+        String branch = device.getBuildAlias().toStringUtf8();
+        String buildFlavor = device.getBuildFlavor().toStringUtf8();
+        String product = device.getProductVariant().toStringUtf8();
+        String buildId = device.getBuildId().toStringUtf8();
+        String abiBitness = device.getAbiBitness().toStringUtf8();
+        String abiName = device.getAbiName().toStringUtf8();
+        return new DeviceInfoEntity(
+                parentKey, branch, product, buildFlavor, buildId, abiBitness, abiName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof DeviceInfoEntity)) {
+            return false;
+        }
+        DeviceInfoEntity device2 = (DeviceInfoEntity) obj;
+        if (!this.branch.equals(device2.branch) || !this.product.equals(device2.product)
+                || !this.buildFlavor.equals(device2.buildFlavor)
+                || !this.buildId.equals(device2.buildId)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        String deviceId = this.branch + this.product + this.buildFlavor + this.buildId;
+        return deviceId.hashCode();
+    }
+
+    /**
+     * Create a copy of the device info under a near parent.
+     * @param parentKey The new parent key.
+     * @return A copy of the DeviceInfoEntity with the specified parent.
+     */
+    public DeviceInfoEntity copyWithParent(Key parentKey) {
+        return new DeviceInfoEntity(parentKey, this.branch, this.product, this.buildFlavor,
+                this.buildId, this.abiBitness, this.abiName);
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
new file mode 100644
index 0000000..fdbbb29
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.android.vts.proto.VtsReportMessage.ProfilingReportMessage;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingType;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.protobuf.ByteString;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing a profiling point execution. */
+public class ProfilingPointRunEntity implements DashboardEntity {
+    protected static final Logger logger =
+            Logger.getLogger(ProfilingPointRunEntity.class.getName());
+
+    public static final String KIND = "ProfilingPointRun";
+
+    // Property keys
+    public static final String TYPE = "type";
+    public static final String REGRESSION_MODE = "regressionMode";
+    public static final String LABELS = "labels";
+    public static final String VALUES = "values";
+    public static final String X_LABEL = "xLabel";
+    public static final String Y_LABEL = "yLabel";
+    public static final String OPTIONS = "options";
+
+    private final Key parentKey;
+
+    public final String name;
+    public final VtsProfilingType type;
+    public final VtsProfilingRegressionMode regressionMode;
+    public final List<String> labels;
+    public final List<Long> values;
+    public final String xLabel;
+    public final String yLabel;
+    public final List<String> options;
+
+    /**
+     * Create a ProfilingPointRunEntity object.
+     *
+     * @param parentKey The Key object for the parent TestRunEntity in the database.
+     * @param name The name of the profiling point.
+     * @param type The (number) type of the profiling point data.
+     * @param regressionMode The (number) mode to use for detecting regression.
+     * @param labels List of data labels, or null if the data is unlabeled.
+     * @param values List of data values.
+     * @param xLabel The x axis label.
+     * @param yLabel The y axis label.
+     * @param options The list of key=value options for the profiling point run.
+     */
+    public ProfilingPointRunEntity(Key parentKey, String name, int type, int regressionMode,
+            List<String> labels, List<Long> values, String xLabel, String yLabel,
+            List<String> options) {
+        this.parentKey = parentKey;
+        this.name = name;
+        this.type = VtsProfilingType.valueOf(type);
+        this.regressionMode = VtsProfilingRegressionMode.valueOf(regressionMode);
+        this.labels = labels == null ? null : new ArrayList<>(labels);
+        this.values = new ArrayList<>(values);
+        this.xLabel = xLabel;
+        this.yLabel = yLabel;
+        this.options = options;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity profilingRun = new Entity(KIND, this.name, this.parentKey);
+        profilingRun.setUnindexedProperty(TYPE, this.type.getNumber());
+        profilingRun.setUnindexedProperty(REGRESSION_MODE, this.regressionMode.getNumber());
+        if (this.labels != null) {
+            profilingRun.setUnindexedProperty(LABELS, this.labels);
+        }
+        profilingRun.setUnindexedProperty(VALUES, this.values);
+        profilingRun.setUnindexedProperty(X_LABEL, this.xLabel);
+        profilingRun.setUnindexedProperty(Y_LABEL, this.yLabel);
+        if (this.options != null) {
+            profilingRun.setUnindexedProperty(OPTIONS, this.options);
+        }
+
+        return profilingRun;
+    }
+
+    /**
+     * Convert an Entity object to a ProflilingPointRunEntity.
+     *
+     * @param e The entity to process.
+     * @return ProfilingPointRunEntity object with the properties from e, or null if incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static ProfilingPointRunEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || e.getKey().getName() == null || !e.hasProperty(TYPE)
+                || !e.hasProperty(REGRESSION_MODE) || !e.hasProperty(VALUES)
+                || !e.hasProperty(X_LABEL) || !e.hasProperty(Y_LABEL)) {
+            logger.log(
+                    Level.WARNING, "Missing profiling point attributes in entity: " + e.toString());
+            return null;
+        }
+        try {
+            Key parentKey = e.getParent();
+            String name = e.getKey().getName();
+            int type = (int) (long) e.getProperty(TYPE);
+            int regressionMode = (int) (long) e.getProperty(REGRESSION_MODE);
+            List<Long> values = (List<Long>) e.getProperty(VALUES);
+            String xLabel = (String) e.getProperty(X_LABEL);
+            String yLabel = (String) e.getProperty(Y_LABEL);
+            List<String> labels = null;
+            if (e.hasProperty(LABELS)) {
+                labels = (List<String>) e.getProperty(LABELS);
+            }
+            List<String> options = null;
+            if (e.hasProperty(OPTIONS)) {
+                options = (List<String>) e.getProperty(OPTIONS);
+            }
+            return new ProfilingPointRunEntity(
+                    parentKey, name, type, regressionMode, labels, values, xLabel, yLabel, options);
+        } catch (ClassCastException exception) {
+            // Invalid cast
+            logger.log(Level.WARNING, "Error parsing profiling point run entity.", exception);
+        }
+        return null;
+    }
+
+    /**
+     * Convert a coverage report to a CoverageEntity.
+     *
+     * @param parentKey The ancestor key for the coverage entity.
+     * @param profilingReport The profiling report containing profiling data.
+     * @return The ProfilingPointRunEntity for the profiling report message, or null if incompatible
+     */
+    public static ProfilingPointRunEntity fromProfilingReport(
+            Key parentKey, ProfilingReportMessage profilingReport) {
+        if (!profilingReport.hasName() || !profilingReport.hasType()
+                || profilingReport.getType() == VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE
+                || !profilingReport.hasRegressionMode() || !profilingReport.hasXAxisLabel()
+                || !profilingReport.hasYAxisLabel()) {
+            return null; // invalid profiling report;
+        }
+        String name = profilingReport.getName().toStringUtf8();
+        VtsProfilingType type = profilingReport.getType();
+        VtsProfilingRegressionMode regressionMode = profilingReport.getRegressionMode();
+        String xLabel = profilingReport.getXAxisLabel().toStringUtf8();
+        String yLabel = profilingReport.getYAxisLabel().toStringUtf8();
+        List<Long> values;
+        List<String> labels = null;
+        switch (type) {
+            case VTS_PROFILING_TYPE_TIMESTAMP:
+                if (!profilingReport.hasStartTimestamp() || !profilingReport.hasEndTimestamp()
+                        || profilingReport.getEndTimestamp()
+                                < profilingReport.getStartTimestamp()) {
+                    return null; // missing timestamp
+                }
+                long value =
+                        profilingReport.getEndTimestamp() - profilingReport.getStartTimestamp();
+                values = new ArrayList<>();
+                values.add(value);
+                break;
+            case VTS_PROFILING_TYPE_LABELED_VECTOR:
+                if (profilingReport.getValueCount() != profilingReport.getLabelCount()) {
+                    return null; // jagged data
+                }
+                labels = new ArrayList<>();
+                for (ByteString label : profilingReport.getLabelList()) {
+                    labels.add(label.toStringUtf8());
+                }
+                values = profilingReport.getValueList();
+                break;
+            case VTS_PROFILING_TYPE_UNLABELED_VECTOR:
+                values = profilingReport.getValueList();
+                break;
+            default: // should never happen
+                return null;
+        }
+        List<String> options = null;
+        if (profilingReport.getOptionsCount() > 0) {
+            options = new ArrayList<>();
+            for (ByteString optionBytes : profilingReport.getOptionsList()) {
+                options.add(optionBytes.toStringUtf8());
+            }
+        }
+        return new ProfilingPointRunEntity(parentKey, name, type.getNumber(),
+                regressionMode.getNumber(), labels, values, xLabel, yLabel, options);
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/TestCaseRunEntity.java b/src/main/java/com/android/vts/entity/TestCaseRunEntity.java
new file mode 100644
index 0000000..02fac2d
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/TestCaseRunEntity.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing the execution of a test case. */
+public class TestCaseRunEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(TestCaseRunEntity.class.getName());
+
+    public static final String KIND = "TestCaseRun";
+
+    // Property keys
+    public static final String TEST_CASE_NAME = "testCaseName";
+    public static final String RESULT = "result";
+    public static final String TEST_CASE_NAMES = "testCaseNames";
+    public static final String RESULTS = "results";
+    public static final String SYSTRACE_URL = "systraceUrl";
+
+    // Maximum number of test cases in the entity.
+    private static final int SIZE_LIMIT = 500;
+
+    public final Key key;
+    public final List<TestCase> testCases;
+    private String systraceUrl;
+
+    /**
+     * Class describing an individual test case run.
+     */
+    public static class TestCase {
+        public final long parentId;
+        public final int offset;
+        public final String name;
+        public final int result;
+
+        /**
+         * Create a test case run.
+         * @param parentId The ID of the TestCaseRunEntity containing the test case.
+         * @param offset The offset of the TestCase into the TestCaseRunEntity.
+         * @param name The name of the test case.
+         * @param result The result of the test case.
+         */
+        public TestCase(long parentId, int offset, String name, int result) {
+            this.parentId = parentId;
+            this.offset = offset;
+            this.name = name;
+            this.result = result;
+        }
+    }
+
+    /**
+     * Create a TestCaseRunEntity with the specified key.
+     * @param key The key to use for the entity in Cloud Datastore.
+     */
+    public TestCaseRunEntity(Key key) {
+        this.key = key;
+        this.testCases = new ArrayList<>();
+        this.systraceUrl = null;
+    }
+
+    /**
+     * Determine if the TestCaseRunEntity is full.
+     * @return True if the entity is full, false otherwise.
+     */
+    public boolean isFull() {
+        return this.testCases.size() >= SIZE_LIMIT;
+    }
+
+    /**
+     * Set the systrace url.
+     * @param url The systrace url, or null.
+     */
+    public void setSystraceUrl(String url) {
+        this.systraceUrl = url;
+    }
+
+    /**
+     * Get the systrace url.
+     * returns The systrace url, or null.
+     */
+    public String getSystraceUrl() {
+        return this.systraceUrl;
+    }
+
+    /**
+     * Add a test case to the test case run entity.
+     * @param name The name of the test case.
+     * @param result The result of the test case.
+     * @return true if added, false otherwise.
+     */
+    public boolean addTestCase(String name, int result) {
+        if (isFull())
+            return false;
+        long parentId = this.key.getId();
+        this.testCases.add(new TestCase(parentId, this.testCases.size(), name, result));
+        return true;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity testCaseRunEntity = new Entity(key);
+        if (this.testCases.size() > 0) {
+            List<String> testCaseNames = new ArrayList<>();
+            List<Integer> results = new ArrayList<>();
+            for (TestCase testCase : this.testCases) {
+                testCaseNames.add(testCase.name);
+                results.add(testCase.result);
+            }
+            testCaseRunEntity.setUnindexedProperty(TEST_CASE_NAMES, testCaseNames);
+            testCaseRunEntity.setUnindexedProperty(RESULTS, results);
+        }
+        if (systraceUrl != null) {
+            testCaseRunEntity.setUnindexedProperty(SYSTRACE_URL, this.systraceUrl);
+        }
+
+        return testCaseRunEntity;
+    }
+
+    /**
+     * Convert an Entity object to a TestCaseRunEntity.
+     *
+     * @param e The entity to process.
+     * @return TestCaseRunEntity object with the properties from e, or null if incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static TestCaseRunEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND)) {
+            logger.log(Level.WARNING, "Wrong kind: " + e.getKey());
+            return null;
+        }
+        try {
+            TestCaseRunEntity testCaseRun = new TestCaseRunEntity(e.getKey());
+            if (e.hasProperty(TEST_CASE_NAMES) && e.hasProperty(RESULTS)) {
+                List<String> testCaseNames = (List<String>) e.getProperty(TEST_CASE_NAMES);
+                List<Long> results = (List<Long>) e.getProperty(RESULTS);
+                if (testCaseNames.size() == results.size()) {
+                    for (int i = 0; i < testCaseNames.size(); i++) {
+                        testCaseRun.addTestCase(testCaseNames.get(i), results.get(i).intValue());
+                    }
+                }
+            }
+            if (e.hasProperty(TEST_CASE_NAME) && e.hasProperty(RESULT)) {
+                testCaseRun.addTestCase(
+                        (String) e.getProperty(TEST_CASE_NAME), (int) (long) e.getProperty(RESULT));
+            }
+            if (e.hasProperty(SYSTRACE_URL)) {
+                String systraceUrl = (String) e.getProperty(SYSTRACE_URL);
+                testCaseRun.setSystraceUrl(systraceUrl);
+            }
+            return testCaseRun;
+        } catch (ClassCastException exception) {
+            // Invalid cast
+            logger.log(Level.WARNING, "Error parsing test case run entity.", exception);
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/TestEntity.java b/src/main/java/com/android/vts/entity/TestEntity.java
new file mode 100644
index 0000000..a5664f4
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/TestEntity.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.android.vts.entity.TestCaseRunEntity.TestCase;
+import com.google.appengine.api.datastore.Entity;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing test metadata. */
+public class TestEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(TestEntity.class.getName());
+
+    public static final String KIND = "Test";
+
+    // Property keys
+    public static final String PASS_COUNT = "passCount";
+    public static final String FAIL_COUNT = "failCount";
+    public static final String UPDATED_TIMESTAMP = "updatedTimestamp";
+
+    protected static final String FAILING_IDS = "failingTestcaseIds";
+    protected static final String FAILING_OFFSETS = "failingTestcaseOffsets";
+
+    public final String testName;
+    public final int passCount;
+    public final int failCount;
+    public final long timestamp;
+    public final List<TestCaseReference> failingTestCases;
+
+    /**
+     * Object representing a reference to a test case.
+     */
+    public static class TestCaseReference {
+        public final long parentId;
+        public final int offset;
+
+        /**
+         * Create a test case reference.
+         * @param parentId The ID of the TestCaseRunEntity containing the test case.
+         * @param offset The offset of the test case into the TestCaseRunEntity.
+         */
+        public TestCaseReference(long parentId, int offset) {
+            this.parentId = parentId;
+            this.offset = offset;
+        }
+
+        /**
+         * Create a test case reference.
+         * @param testCase The TestCase to reference.
+         */
+        public TestCaseReference(TestCase testCase) {
+            this(testCase.parentId, testCase.offset);
+        }
+    }
+
+    /**
+     * Create a TestEntity object with status metadata.
+     *
+     * @param testName The name of the test.
+     * @param timestamp The timestamp indicating the most recent test run event in the test state.
+     * @param passCount The number of tests passing up to the timestamp specified.
+     * @param failCount The number of tests failing up to the timestamp specified.
+     * @param failingTestCases The TestCaseReferences of the last observed failing test cases.
+     */
+    public TestEntity(String testName, long timestamp, int passCount, int failCount,
+            List<TestCaseReference> failingTestCases) {
+        this.testName = testName;
+        this.timestamp = timestamp;
+        this.passCount = passCount;
+        this.failCount = failCount;
+        this.failingTestCases = failingTestCases;
+    }
+
+    /**
+     * Create a TestEntity object without metadata.
+     *
+     * @param testName The name of the test.
+     */
+    public TestEntity(String testName) {
+        this(testName, 0, -1, -1, new ArrayList<TestCaseReference>());
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity testEntity = new Entity(KIND, this.testName);
+        if (this.timestamp >= 0 && this.passCount >= 0 && this.failCount >= 0) {
+            testEntity.setProperty(UPDATED_TIMESTAMP, this.timestamp);
+            testEntity.setProperty(PASS_COUNT, this.passCount);
+            testEntity.setProperty(FAIL_COUNT, this.failCount);
+            if (this.failingTestCases.size() > 0) {
+                List<Long> failingTestcaseIds = new ArrayList<>();
+                List<Integer> failingTestcaseOffsets = new ArrayList<>();
+                for (TestCaseReference testCase : this.failingTestCases) {
+                    failingTestcaseIds.add(testCase.parentId);
+                    failingTestcaseOffsets.add(testCase.offset);
+                }
+                testEntity.setUnindexedProperty(FAILING_IDS, failingTestcaseIds);
+                testEntity.setUnindexedProperty(FAILING_OFFSETS, failingTestcaseOffsets);
+            }
+        }
+        return testEntity;
+    }
+
+    /**
+     * Convert an Entity object to a TestEntity.
+     *
+     * @param e The entity to process.
+     * @return TestEntity object with the properties from e processed, or null if incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static TestEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || e.getKey().getName() == null) {
+            logger.log(Level.WARNING, "Missing test attributes in entity: " + e.toString());
+            return null;
+        }
+        String testName = e.getKey().getName();
+        long timestamp = 0;
+        int passCount = -1;
+        int failCount = -1;
+        List<TestCaseReference> failingTestCases = new ArrayList<>();
+        try {
+            if (e.hasProperty(UPDATED_TIMESTAMP)) {
+                timestamp = (long) e.getProperty(UPDATED_TIMESTAMP);
+            }
+            if (e.hasProperty(PASS_COUNT)) {
+                passCount = ((Long) e.getProperty(PASS_COUNT)).intValue();
+            }
+            if (e.hasProperty(FAIL_COUNT)) {
+                failCount = ((Long) e.getProperty(FAIL_COUNT)).intValue();
+            }
+            if (e.hasProperty(FAILING_IDS) && e.hasProperty(FAILING_OFFSETS)) {
+                List<Long> ids = (List<Long>) e.getProperty(FAILING_IDS);
+                List<Long> offsets = (List<Long>) e.getProperty(FAILING_OFFSETS);
+                if (ids.size() == offsets.size()) {
+                    for (int i = 0; i < ids.size(); i++) {
+                        failingTestCases.add(
+                                new TestCaseReference(ids.get(i), offsets.get(i).intValue()));
+                    }
+                }
+            }
+        } catch (ClassCastException exception) {
+            // Invalid contents or null values
+            logger.log(Level.WARNING, "Error parsing test entity.", exception);
+        }
+        return new TestEntity(testName, timestamp, passCount, failCount, failingTestCases);
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/TestPlanEntity.java b/src/main/java/com/android/vts/entity/TestPlanEntity.java
new file mode 100644
index 0000000..82d7e5e
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/TestPlanEntity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.google.appengine.api.datastore.Entity;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing test plan metadata. */
+public class TestPlanEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(TestPlanEntity.class.getName());
+
+    public static final String KIND = "TestPlan";
+
+    // Property keys
+    public static final String TEST_PLAN_NAME = "testPlanName";
+
+    public final String testPlanName;
+
+    /**
+     * Create a TestPlanEntity object.
+     *
+     * @param testPlanName The name of the test plan.
+     */
+    public TestPlanEntity(String testPlanName) {
+        this.testPlanName = testPlanName;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity planEntity = new Entity(KIND, this.testPlanName);
+        return planEntity;
+    }
+
+    /**
+     * Convert an Entity object to a TestEntity.
+     *
+     * @param e The entity to process.
+     * @return TestEntity object with the properties from e processed, or null if incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static TestPlanEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || e.getKey().getName() == null
+                || !e.hasProperty(TEST_PLAN_NAME)) {
+            logger.log(Level.WARNING, "Missing test plan attributes in entity: " + e.toString());
+            return null;
+        }
+        String testPlanName = e.getKey().getName();
+        return new TestPlanEntity(testPlanName);
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/TestPlanRunEntity.java b/src/main/java/com/android/vts/entity/TestPlanRunEntity.java
new file mode 100644
index 0000000..cc161ac
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/TestPlanRunEntity.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.android.vts.entity.TestRunEntity.TestRunType;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing test plan run information. */
+public class TestPlanRunEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(TestPlanRunEntity.class.getName());
+
+    public static final String KIND = "TestPlanRun";
+
+    // Property keys
+    public static final String TEST_PLAN_NAME = "testPlanName";
+    public static final String TYPE = "type";
+    public static final String START_TIMESTAMP = "startTimestamp";
+    public static final String END_TIMESTAMP = "endTimestamp";
+    public static final String TEST_BUILD_ID = "testBuildId";
+    public static final String PASS_COUNT = "passCount";
+    public static final String FAIL_COUNT = "failCount";
+    public static final String TEST_RUNS = "testRuns";
+
+    public final Key key;
+    public final String testPlanName;
+    public final TestRunType type;
+    public final long startTimestamp;
+    public final long endTimestamp;
+    public final String testBuildId;
+    public final long passCount;
+    public final long failCount;
+    public final List<Key> testRuns;
+
+    /**
+     * Create a TestPlanRunEntity object describing a test plan run.
+     *
+     * @param parentKey The key for the parent entity in the database.
+     * @param type The test run type (e.g. presubmit, postsubmit, other)
+     * @param startTimestamp The time in microseconds when the test plan run started.
+     * @param endTimestamp The time in microseconds when the test plan run ended.
+     * @param testBuildId The build ID of the VTS test build.
+     * @param passCount The number of passing test cases in the run.
+     * @param failCount The number of failing test cases in the run.
+     * @param testRuns A list of keys to the TestRunEntity objects for the plan run run.
+     */
+    public TestPlanRunEntity(Key parentKey, String testPlanName, TestRunType type,
+            long startTimestamp, long endTimestamp, String testBuildId, long passCount,
+            long failCount, List<Key> testRuns) {
+        this.key = KeyFactory.createKey(parentKey, KIND, startTimestamp);
+        this.testPlanName = testPlanName;
+        this.type = type;
+        this.startTimestamp = startTimestamp;
+        this.endTimestamp = endTimestamp;
+        this.testBuildId = testBuildId;
+        this.passCount = passCount;
+        this.failCount = failCount;
+        this.testRuns = testRuns;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity planRun = new Entity(this.key);
+        planRun.setProperty(TEST_PLAN_NAME, this.testPlanName);
+        planRun.setProperty(TYPE, this.type.getNumber());
+        planRun.setProperty(START_TIMESTAMP, this.startTimestamp);
+        planRun.setProperty(END_TIMESTAMP, this.endTimestamp);
+        planRun.setProperty(TEST_BUILD_ID, this.testBuildId.toLowerCase());
+        planRun.setProperty(PASS_COUNT, this.passCount);
+        planRun.setProperty(FAIL_COUNT, this.failCount);
+        if (this.testRuns != null && this.testRuns.size() > 0) {
+            planRun.setUnindexedProperty(TEST_RUNS, this.testRuns);
+        }
+        return planRun;
+    }
+
+    /**
+     * Convert an Entity object to a TestPlanRunEntity.
+     *
+     * @param e The entity to process.
+     * @return TestPlanRunEntity object with the properties from e processed, or null if
+     * incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static TestPlanRunEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || !e.hasProperty(TEST_PLAN_NAME) || !e.hasProperty(TYPE)
+                || !e.hasProperty(START_TIMESTAMP) || !e.hasProperty(END_TIMESTAMP)
+                || !e.hasProperty(TEST_BUILD_ID) || !e.hasProperty(PASS_COUNT)
+                || !e.hasProperty(FAIL_COUNT) || !e.hasProperty(TEST_RUNS)) {
+            logger.log(Level.WARNING, "Missing test run attributes in entity: " + e.toString());
+            return null;
+        }
+        try {
+            String testPlanName = (String) e.getProperty(TEST_PLAN_NAME);
+            TestRunType type = TestRunType.fromNumber((int) (long) e.getProperty(TYPE));
+            long startTimestamp = (long) e.getProperty(START_TIMESTAMP);
+            long endTimestamp = (long) e.getProperty(END_TIMESTAMP);
+            String testBuildId = (String) e.getProperty(TEST_BUILD_ID);
+            long passCount = (long) e.getProperty(PASS_COUNT);
+            long failCount = (long) e.getProperty(FAIL_COUNT);
+            List<Key> testRuns = (List<Key>) e.getProperty(TEST_RUNS);
+            return new TestPlanRunEntity(e.getKey().getParent(), testPlanName, type, startTimestamp,
+                    endTimestamp, testBuildId, passCount, failCount, testRuns);
+        } catch (ClassCastException exception) {
+            // Invalid cast
+            logger.log(Level.WARNING, "Error parsing test plan run entity.", exception);
+        }
+        return null;
+    }
+
+    public JsonObject toJson() {
+        JsonObject json = new JsonObject();
+        json.add(TEST_PLAN_NAME, new JsonPrimitive(this.testPlanName));
+        json.add(TEST_BUILD_ID, new JsonPrimitive(this.testBuildId));
+        json.add(PASS_COUNT, new JsonPrimitive(this.passCount));
+        json.add(FAIL_COUNT, new JsonPrimitive(this.failCount));
+        json.add(START_TIMESTAMP, new JsonPrimitive(this.startTimestamp));
+        json.add(END_TIMESTAMP, new JsonPrimitive(this.endTimestamp));
+        return json;
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/TestRunEntity.java b/src/main/java/com/android/vts/entity/TestRunEntity.java
new file mode 100644
index 0000000..8bb94a8
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/TestRunEntity.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.android.vts.util.UrlUtil;
+import com.android.vts.util.UrlUtil.LinkDisplay;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing test run information. */
+public class TestRunEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(TestRunEntity.class.getName());
+
+    /** Enum for classifying test run types. */
+    public enum TestRunType {
+        OTHER(0),
+        PRESUBMIT(1),
+        POSTSUBMIT(2);
+
+        private final int value;
+
+        private TestRunType(int value) {
+            this.value = value;
+        }
+
+        /**
+         * Get the ordinal representation of the type.
+         *
+         * @return The value associated with the test run type.
+         */
+        public int getNumber() {
+            return value;
+        }
+
+        /**
+         * Convert an ordinal value to a TestRunType.
+         *
+         * @param value The orginal value to parse.
+         * @return a TestRunType value.
+         */
+        public static TestRunType fromNumber(int value) {
+            if (value == 1) {
+                return TestRunType.PRESUBMIT;
+            } else if (value == 2) {
+                return TestRunType.POSTSUBMIT;
+            } else {
+                return TestRunType.OTHER;
+            }
+        }
+
+        /**
+         * Determine the test run type based on the build ID.
+         *
+         * Postsubmit runs are expected to have integer build IDs, while presubmit runs are integers
+         * prefixed by the character P. All other runs (e.g. local builds) are classified as OTHER.
+         *
+         * @param buildId The build ID.
+         * @return the TestRunType.
+         */
+        public static TestRunType fromBuildId(String buildId) {
+            try {
+                Integer.parseInt(buildId);
+                return TestRunType.POSTSUBMIT;
+            } catch (NumberFormatException e) {
+                // Not an integer
+            }
+            if (Character.toLowerCase(buildId.charAt(0)) == 'p') {
+                try {
+                    Integer.parseInt(buildId.substring(1));
+                    return TestRunType.PRESUBMIT;
+                } catch (NumberFormatException e) {
+                    // Not an integer
+                }
+            }
+            return TestRunType.OTHER;
+        }
+    }
+
+    public static final String KIND = "TestRun";
+
+    // Property keys
+    public static final String TEST_NAME = "testName";
+    public static final String TYPE = "type";
+    public static final String START_TIMESTAMP = "startTimestamp";
+    public static final String END_TIMESTAMP = "endTimestamp";
+    public static final String TEST_BUILD_ID = "testBuildId";
+    public static final String HOST_NAME = "hostName";
+    public static final String PASS_COUNT = "passCount";
+    public static final String FAIL_COUNT = "failCount";
+    public static final String TEST_CASE_IDS = "testCaseIds";
+    public static final String LOG_LINKS = "logLinks";
+    public static final String HAS_COVERAGE = "hasCoverage";
+    public static final String TOTAL_LINE_COUNT = "totalLineCount";
+    public static final String COVERED_LINE_COUNT = "coveredLineCount";
+
+    public final Key key;
+    public final TestRunType type;
+    public final long startTimestamp;
+    public final long endTimestamp;
+    public final String testBuildId;
+    public final String hostName;
+    public final long passCount;
+    public final long failCount;
+    public final boolean hasCoverage;
+    public final long coveredLineCount;
+    public final long totalLineCount;
+    public final List<Long> testCaseIds;
+    public final List<String> logLinks;
+
+    /**
+     * Create a TestRunEntity object describing a test run.
+     *
+     * @param parentKey The key to the parent TestEntity.
+     * @param type The test run type (e.g. presubmit, postsubmit, other)
+     * @param startTimestamp The time in microseconds when the test run started.
+     * @param endTimestamp The time in microseconds when the test run ended.
+     * @param testBuildId The build ID of the VTS test build.
+     * @param hostName The name of host machine.
+     * @param passCount The number of passing test cases in the run.
+     * @param failCount The number of failing test cases in the run.
+     * @param testCaseIds A list of key IDs to the TestCaseRunEntity objects for the test run.
+     * @param logLinks A list of links to log files for the test run, or null if there aren't any.
+     * @param coveredLineCount The number of lines covered by the test run.
+     * @param totalLineCount The total number of executable lines by the test in the test run.
+     */
+    public TestRunEntity(Key parentKey, TestRunType type, long startTimestamp, long endTimestamp,
+            String testBuildId, String hostName, long passCount, long failCount,
+            List<Long> testCaseIds, List<String> logLinks, long coveredLineCount,
+            long totalLineCount) {
+        this.key = KeyFactory.createKey(parentKey, KIND, startTimestamp);
+        this.type = type;
+        this.startTimestamp = startTimestamp;
+        this.endTimestamp = endTimestamp;
+        this.testBuildId = testBuildId;
+        this.hostName = hostName;
+        this.passCount = passCount;
+        this.failCount = failCount;
+        this.testCaseIds = testCaseIds;
+        this.logLinks = logLinks;
+        this.coveredLineCount = coveredLineCount;
+        this.totalLineCount = totalLineCount;
+        this.hasCoverage = totalLineCount > 0;
+    }
+
+    /**
+     * Create a TestRunEntity object describing a test run.
+     *
+     * @param parentKey The key to the parent TestEntity.
+     * @param type The test run type (e.g. presubmit, postsubmit, other)
+     * @param startTimestamp The time in microseconds when the test run started.
+     * @param endTimestamp The time in microseconds when the test run ended.
+     * @param testBuildId The build ID of the VTS test build.
+     * @param hostName The name of host machine.
+     * @param passCount The number of passing test cases in the run.
+     * @param failCount The number of failing test cases in the run.
+     * @param testCaseIds A list of key IDs to the TestCaseRunEntity objects for the test run.
+     * @param logLinks A list of links to log files for the test run, or null if there aren't any.
+     */
+    public TestRunEntity(Key parentKey, TestRunType type, long startTimestamp, long endTimestamp,
+            String testBuildId, String hostName, long passCount, long failCount,
+            List<Long> testCaseIds, List<String> logLinks) {
+        this(parentKey, type, startTimestamp, endTimestamp, testBuildId, hostName, passCount,
+                failCount, testCaseIds, logLinks, 0, 0);
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity testRunEntity = new Entity(this.key);
+        testRunEntity.setProperty(TEST_NAME, this.key.getParent().getName());
+        testRunEntity.setProperty(TYPE, this.type.getNumber());
+        testRunEntity.setProperty(START_TIMESTAMP, this.startTimestamp);
+        testRunEntity.setUnindexedProperty(END_TIMESTAMP, this.endTimestamp);
+        testRunEntity.setProperty(TEST_BUILD_ID, this.testBuildId.toLowerCase());
+        testRunEntity.setProperty(HOST_NAME, this.hostName.toLowerCase());
+        testRunEntity.setProperty(PASS_COUNT, this.passCount);
+        testRunEntity.setProperty(FAIL_COUNT, this.failCount);
+        testRunEntity.setUnindexedProperty(TEST_CASE_IDS, this.testCaseIds);
+        boolean hasCoverage = this.totalLineCount > 0 && this.coveredLineCount >= 0;
+        testRunEntity.setProperty(HAS_COVERAGE, hasCoverage);
+        if (hasCoverage) {
+            testRunEntity.setProperty(COVERED_LINE_COUNT, this.coveredLineCount);
+            testRunEntity.setProperty(TOTAL_LINE_COUNT, this.totalLineCount);
+        }
+        if (this.logLinks != null && this.logLinks.size() > 0) {
+            testRunEntity.setUnindexedProperty(LOG_LINKS, this.logLinks);
+        }
+        return testRunEntity;
+    }
+
+    /**
+     * Convert an Entity object to a TestRunEntity.
+     *
+     * @param e The entity to process.
+     * @return TestRunEntity object with the properties from e processed, or null if incompatible.
+     */
+    @SuppressWarnings("unchecked")
+    public static TestRunEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || !e.hasProperty(TYPE) || !e.hasProperty(START_TIMESTAMP)
+                || !e.hasProperty(END_TIMESTAMP) || !e.hasProperty(TEST_BUILD_ID)
+                || !e.hasProperty(HOST_NAME) || !e.hasProperty(PASS_COUNT)
+                || !e.hasProperty(FAIL_COUNT) || !e.hasProperty(TEST_CASE_IDS)) {
+            logger.log(Level.WARNING, "Missing test run attributes in entity: " + e.toString());
+            return null;
+        }
+        try {
+            TestRunType type = TestRunType.fromNumber((int) (long) e.getProperty(TYPE));
+            long startTimestamp = (long) e.getProperty(START_TIMESTAMP);
+            long endTimestamp = (long) e.getProperty(END_TIMESTAMP);
+            String testBuildId = (String) e.getProperty(TEST_BUILD_ID);
+            String hostName = (String) e.getProperty(HOST_NAME);
+            long passCount = (long) e.getProperty(PASS_COUNT);
+            long failCount = (long) e.getProperty(FAIL_COUNT);
+            List<Long> testCaseIds = (List<Long>) e.getProperty(TEST_CASE_IDS);
+            List<String> logLinks = null;
+            long coveredLineCount = 0;
+            long totalLineCount = 0;
+            if (e.hasProperty(TOTAL_LINE_COUNT) && e.hasProperty(COVERED_LINE_COUNT)) {
+                coveredLineCount = (long) e.getProperty(COVERED_LINE_COUNT);
+                totalLineCount = (long) e.getProperty(TOTAL_LINE_COUNT);
+            }
+            if (e.hasProperty(LOG_LINKS)) {
+                logLinks = (List<String>) e.getProperty(LOG_LINKS);
+            }
+            return new TestRunEntity(e.getKey().getParent(), type, startTimestamp, endTimestamp,
+                    testBuildId, hostName, passCount, failCount, testCaseIds, logLinks,
+                    coveredLineCount, totalLineCount);
+        } catch (ClassCastException exception) {
+            // Invalid cast
+            logger.log(Level.WARNING, "Error parsing test run entity.", exception);
+        }
+        return null;
+    }
+
+    public JsonObject toJson() {
+        JsonObject json = new JsonObject();
+        json.add(TEST_NAME, new JsonPrimitive(this.key.getParent().getName()));
+        json.add(TEST_BUILD_ID, new JsonPrimitive(this.testBuildId));
+        json.add(HOST_NAME, new JsonPrimitive(this.hostName));
+        json.add(PASS_COUNT, new JsonPrimitive(this.passCount));
+        json.add(FAIL_COUNT, new JsonPrimitive(this.failCount));
+        json.add(START_TIMESTAMP, new JsonPrimitive(this.startTimestamp));
+        json.add(END_TIMESTAMP, new JsonPrimitive(this.endTimestamp));
+        if (this.totalLineCount > 0 && this.coveredLineCount >= 0) {
+            json.add(COVERED_LINE_COUNT, new JsonPrimitive(this.coveredLineCount));
+            json.add(TOTAL_LINE_COUNT, new JsonPrimitive(this.totalLineCount));
+        }
+        if (this.logLinks != null && this.logLinks.size() > 0) {
+            List<JsonElement> links = new ArrayList<>();
+            for (String rawUrl : this.logLinks) {
+                LinkDisplay validatedLink = UrlUtil.processUrl(rawUrl);
+                if (validatedLink == null) {
+                    logger.log(Level.WARNING, "Invalid logging URL : " + rawUrl);
+                    continue;
+                }
+                String[] logInfo = new String[] {validatedLink.name, validatedLink.url};
+                links.add(new Gson().toJsonTree(logInfo));
+            }
+            if (links.size() > 0) {
+                json.add(LOG_LINKS, new Gson().toJsonTree(links));
+            }
+        }
+        return json;
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/UserFavoriteEntity.java b/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
new file mode 100644
index 0000000..a2820a6
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.entity;
+
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.users.User;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** Entity describing subscriptions between a user and a test. */
+public class UserFavoriteEntity implements DashboardEntity {
+    protected static final Logger logger = Logger.getLogger(UserFavoriteEntity.class.getName());
+
+    public static final String KIND = "UserFavorite";
+
+    // Property keys
+    public static final String USER = "user";
+    public static final String TEST_KEY = "testKey";
+
+    public final User user;
+    public final Key testKey;
+
+    /**
+     * Create a user favorite relationship.
+     *
+     * @param user The User object for the subscribing user.
+     * @param testKey The key of the TestEntity object describing the test.
+     */
+    public UserFavoriteEntity(User user, Key testKey) {
+        this.user = user;
+        this.testKey = testKey;
+    }
+
+    @Override
+    public Entity toEntity() {
+        Entity favoriteEntity = new Entity(KIND);
+        favoriteEntity.setProperty(USER, this.user);
+        favoriteEntity.setProperty(TEST_KEY, this.testKey);
+        return favoriteEntity;
+    }
+
+    /**
+     * Convert an Entity object to a UserFavoriteEntity.
+     *
+     * @param e The entity to process.
+     * @return UserFavoriteEntity object with the properties from e, or null if incompatible.
+     */
+    public static UserFavoriteEntity fromEntity(Entity e) {
+        if (!e.getKind().equals(KIND) || !e.hasProperty(USER) || !e.hasProperty(TEST_KEY)) {
+            logger.log(
+                    Level.WARNING, "Missing user favorite attributes in entity: " + e.toString());
+            return null;
+        }
+        try {
+            User user = (User) e.getProperty(USER);
+            Key testKey = (Key) e.getProperty(TEST_KEY);
+            return new UserFavoriteEntity(user, testKey);
+        } catch (ClassCastException exception) {
+            // Invalid cast
+            logger.log(Level.WARNING, "Error parsing user favorite entity.", exception);
+        }
+        return null;
+    }
+}
diff --git a/src/main/java/com/android/vts/proto/VtsReportMessage.java b/src/main/java/com/android/vts/proto/VtsReportMessage.java
new file mode 100644
index 0000000..ac80e86
--- /dev/null
+++ b/src/main/java/com/android/vts/proto/VtsReportMessage.java
@@ -0,0 +1,18604 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: proto/VtsReportMessage.proto
+
+package com.android.vts.proto;
+
+public final class VtsReportMessage {
+  private VtsReportMessage() {}
+  public static void registerAllExtensions(
+      com.google.protobuf.ExtensionRegistry registry) {
+  }
+  /**
+   * Protobuf enum {@code android.vts.TestCaseResult}
+   *
+   * <pre>
+   * To specify test case execution result.
+   * </pre>
+   */
+  public enum TestCaseResult
+      implements com.google.protobuf.ProtocolMessageEnum {
+    /**
+     * <code>UNKNOWN_RESULT = 0;</code>
+     */
+    UNKNOWN_RESULT(0, 0),
+    /**
+     * <code>TEST_CASE_RESULT_PASS = 1;</code>
+     */
+    TEST_CASE_RESULT_PASS(1, 1),
+    /**
+     * <code>TEST_CASE_RESULT_FAIL = 2;</code>
+     */
+    TEST_CASE_RESULT_FAIL(2, 2),
+    /**
+     * <code>TEST_CASE_RESULT_SKIP = 3;</code>
+     */
+    TEST_CASE_RESULT_SKIP(3, 3),
+    /**
+     * <code>TEST_CASE_RESULT_EXCEPTION = 4;</code>
+     */
+    TEST_CASE_RESULT_EXCEPTION(4, 4),
+    /**
+     * <code>TEST_CASE_RESULT_TIMEOUT = 5;</code>
+     */
+    TEST_CASE_RESULT_TIMEOUT(5, 5),
+    ;
+
+    /**
+     * <code>UNKNOWN_RESULT = 0;</code>
+     */
+    public static final int UNKNOWN_RESULT_VALUE = 0;
+    /**
+     * <code>TEST_CASE_RESULT_PASS = 1;</code>
+     */
+    public static final int TEST_CASE_RESULT_PASS_VALUE = 1;
+    /**
+     * <code>TEST_CASE_RESULT_FAIL = 2;</code>
+     */
+    public static final int TEST_CASE_RESULT_FAIL_VALUE = 2;
+    /**
+     * <code>TEST_CASE_RESULT_SKIP = 3;</code>
+     */
+    public static final int TEST_CASE_RESULT_SKIP_VALUE = 3;
+    /**
+     * <code>TEST_CASE_RESULT_EXCEPTION = 4;</code>
+     */
+    public static final int TEST_CASE_RESULT_EXCEPTION_VALUE = 4;
+    /**
+     * <code>TEST_CASE_RESULT_TIMEOUT = 5;</code>
+     */
+    public static final int TEST_CASE_RESULT_TIMEOUT_VALUE = 5;
+
+
+    public final int getNumber() { return value; }
+
+    public static TestCaseResult valueOf(int value) {
+      switch (value) {
+        case 0: return UNKNOWN_RESULT;
+        case 1: return TEST_CASE_RESULT_PASS;
+        case 2: return TEST_CASE_RESULT_FAIL;
+        case 3: return TEST_CASE_RESULT_SKIP;
+        case 4: return TEST_CASE_RESULT_EXCEPTION;
+        case 5: return TEST_CASE_RESULT_TIMEOUT;
+        default: return null;
+      }
+    }
+
+    public static com.google.protobuf.Internal.EnumLiteMap<TestCaseResult>
+        internalGetValueMap() {
+      return internalValueMap;
+    }
+    private static com.google.protobuf.Internal.EnumLiteMap<TestCaseResult>
+        internalValueMap =
+          new com.google.protobuf.Internal.EnumLiteMap<TestCaseResult>() {
+            public TestCaseResult findValueByNumber(int number) {
+              return TestCaseResult.valueOf(number);
+            }
+          };
+
+    public final com.google.protobuf.Descriptors.EnumValueDescriptor
+        getValueDescriptor() {
+      return getDescriptor().getValues().get(index);
+    }
+    public final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptorForType() {
+      return getDescriptor();
+    }
+    public static final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.getDescriptor().getEnumTypes().get(0);
+    }
+
+    private static final TestCaseResult[] VALUES = values();
+
+    public static TestCaseResult valueOf(
+        com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+      if (desc.getType() != getDescriptor()) {
+        throw new java.lang.IllegalArgumentException(
+          "EnumValueDescriptor is not for this type.");
+      }
+      return VALUES[desc.getIndex()];
+    }
+
+    private final int index;
+    private final int value;
+
+    private TestCaseResult(int index, int value) {
+      this.index = index;
+      this.value = value;
+    }
+
+    // @@protoc_insertion_point(enum_scope:android.vts.TestCaseResult)
+  }
+
+  /**
+   * Protobuf enum {@code android.vts.VtsTestType}
+   *
+   * <pre>
+   * To specify the VTS test type.
+   * </pre>
+   */
+  public enum VtsTestType
+      implements com.google.protobuf.ProtocolMessageEnum {
+    /**
+     * <code>UNKNOWN_VTS_TESTTYPE = 0;</code>
+     */
+    UNKNOWN_VTS_TESTTYPE(0, 0),
+    /**
+     * <code>VTS_HOST_DRIVEN_STRUCTURAL = 1;</code>
+     */
+    VTS_HOST_DRIVEN_STRUCTURAL(1, 1),
+    /**
+     * <code>VTS_HOST_DRIVEN_FUZZING = 2;</code>
+     */
+    VTS_HOST_DRIVEN_FUZZING(2, 2),
+    /**
+     * <code>VTS_TARGET_SIDE_GTEST = 3;</code>
+     */
+    VTS_TARGET_SIDE_GTEST(3, 3),
+    /**
+     * <code>VTS_TARGET_SIDE_FUZZING = 4;</code>
+     */
+    VTS_TARGET_SIDE_FUZZING(4, 4),
+    ;
+
+    /**
+     * <code>UNKNOWN_VTS_TESTTYPE = 0;</code>
+     */
+    public static final int UNKNOWN_VTS_TESTTYPE_VALUE = 0;
+    /**
+     * <code>VTS_HOST_DRIVEN_STRUCTURAL = 1;</code>
+     */
+    public static final int VTS_HOST_DRIVEN_STRUCTURAL_VALUE = 1;
+    /**
+     * <code>VTS_HOST_DRIVEN_FUZZING = 2;</code>
+     */
+    public static final int VTS_HOST_DRIVEN_FUZZING_VALUE = 2;
+    /**
+     * <code>VTS_TARGET_SIDE_GTEST = 3;</code>
+     */
+    public static final int VTS_TARGET_SIDE_GTEST_VALUE = 3;
+    /**
+     * <code>VTS_TARGET_SIDE_FUZZING = 4;</code>
+     */
+    public static final int VTS_TARGET_SIDE_FUZZING_VALUE = 4;
+
+
+    public final int getNumber() { return value; }
+
+    public static VtsTestType valueOf(int value) {
+      switch (value) {
+        case 0: return UNKNOWN_VTS_TESTTYPE;
+        case 1: return VTS_HOST_DRIVEN_STRUCTURAL;
+        case 2: return VTS_HOST_DRIVEN_FUZZING;
+        case 3: return VTS_TARGET_SIDE_GTEST;
+        case 4: return VTS_TARGET_SIDE_FUZZING;
+        default: return null;
+      }
+    }
+
+    public static com.google.protobuf.Internal.EnumLiteMap<VtsTestType>
+        internalGetValueMap() {
+      return internalValueMap;
+    }
+    private static com.google.protobuf.Internal.EnumLiteMap<VtsTestType>
+        internalValueMap =
+          new com.google.protobuf.Internal.EnumLiteMap<VtsTestType>() {
+            public VtsTestType findValueByNumber(int number) {
+              return VtsTestType.valueOf(number);
+            }
+          };
+
+    public final com.google.protobuf.Descriptors.EnumValueDescriptor
+        getValueDescriptor() {
+      return getDescriptor().getValues().get(index);
+    }
+    public final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptorForType() {
+      return getDescriptor();
+    }
+    public static final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.getDescriptor().getEnumTypes().get(1);
+    }
+
+    private static final VtsTestType[] VALUES = values();
+
+    public static VtsTestType valueOf(
+        com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+      if (desc.getType() != getDescriptor()) {
+        throw new java.lang.IllegalArgumentException(
+          "EnumValueDescriptor is not for this type.");
+      }
+      return VALUES[desc.getIndex()];
+    }
+
+    private final int index;
+    private final int value;
+
+    private VtsTestType(int index, int value) {
+      this.index = index;
+      this.value = value;
+    }
+
+    // @@protoc_insertion_point(enum_scope:android.vts.VtsTestType)
+  }
+
+  /**
+   * Protobuf enum {@code android.vts.VtsProfilingRegressionMode}
+   */
+  public enum VtsProfilingRegressionMode
+      implements com.google.protobuf.ProtocolMessageEnum {
+    /**
+     * <code>UNKNOWN_REGRESSION_MODE = 0;</code>
+     */
+    UNKNOWN_REGRESSION_MODE(0, 0),
+    /**
+     * <code>VTS_REGRESSION_MODE_DISABLED = 1;</code>
+     *
+     * <pre>
+     * disable analysis
+     * </pre>
+     */
+    VTS_REGRESSION_MODE_DISABLED(1, 1),
+    /**
+     * <code>VTS_REGRESSION_MODE_INCREASING = 2;</code>
+     *
+     * <pre>
+     * interpret increases in values as regression
+     * </pre>
+     */
+    VTS_REGRESSION_MODE_INCREASING(2, 2),
+    /**
+     * <code>VTS_REGRESSION_MODE_DECREASING = 3;</code>
+     *
+     * <pre>
+     * interpret decreases in values as regression
+     * </pre>
+     */
+    VTS_REGRESSION_MODE_DECREASING(3, 3),
+    ;
+
+    /**
+     * <code>UNKNOWN_REGRESSION_MODE = 0;</code>
+     */
+    public static final int UNKNOWN_REGRESSION_MODE_VALUE = 0;
+    /**
+     * <code>VTS_REGRESSION_MODE_DISABLED = 1;</code>
+     *
+     * <pre>
+     * disable analysis
+     * </pre>
+     */
+    public static final int VTS_REGRESSION_MODE_DISABLED_VALUE = 1;
+    /**
+     * <code>VTS_REGRESSION_MODE_INCREASING = 2;</code>
+     *
+     * <pre>
+     * interpret increases in values as regression
+     * </pre>
+     */
+    public static final int VTS_REGRESSION_MODE_INCREASING_VALUE = 2;
+    /**
+     * <code>VTS_REGRESSION_MODE_DECREASING = 3;</code>
+     *
+     * <pre>
+     * interpret decreases in values as regression
+     * </pre>
+     */
+    public static final int VTS_REGRESSION_MODE_DECREASING_VALUE = 3;
+
+
+    public final int getNumber() { return value; }
+
+    public static VtsProfilingRegressionMode valueOf(int value) {
+      switch (value) {
+        case 0: return UNKNOWN_REGRESSION_MODE;
+        case 1: return VTS_REGRESSION_MODE_DISABLED;
+        case 2: return VTS_REGRESSION_MODE_INCREASING;
+        case 3: return VTS_REGRESSION_MODE_DECREASING;
+        default: return null;
+      }
+    }
+
+    public static com.google.protobuf.Internal.EnumLiteMap<VtsProfilingRegressionMode>
+        internalGetValueMap() {
+      return internalValueMap;
+    }
+    private static com.google.protobuf.Internal.EnumLiteMap<VtsProfilingRegressionMode>
+        internalValueMap =
+          new com.google.protobuf.Internal.EnumLiteMap<VtsProfilingRegressionMode>() {
+            public VtsProfilingRegressionMode findValueByNumber(int number) {
+              return VtsProfilingRegressionMode.valueOf(number);
+            }
+          };
+
+    public final com.google.protobuf.Descriptors.EnumValueDescriptor
+        getValueDescriptor() {
+      return getDescriptor().getValues().get(index);
+    }
+    public final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptorForType() {
+      return getDescriptor();
+    }
+    public static final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.getDescriptor().getEnumTypes().get(2);
+    }
+
+    private static final VtsProfilingRegressionMode[] VALUES = values();
+
+    public static VtsProfilingRegressionMode valueOf(
+        com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+      if (desc.getType() != getDescriptor()) {
+        throw new java.lang.IllegalArgumentException(
+          "EnumValueDescriptor is not for this type.");
+      }
+      return VALUES[desc.getIndex()];
+    }
+
+    private final int index;
+    private final int value;
+
+    private VtsProfilingRegressionMode(int index, int value) {
+      this.index = index;
+      this.value = value;
+    }
+
+    // @@protoc_insertion_point(enum_scope:android.vts.VtsProfilingRegressionMode)
+  }
+
+  /**
+   * Protobuf enum {@code android.vts.VtsProfilingType}
+   */
+  public enum VtsProfilingType
+      implements com.google.protobuf.ProtocolMessageEnum {
+    /**
+     * <code>UNKNOWN_VTS_PROFILING_TYPE = 0;</code>
+     */
+    UNKNOWN_VTS_PROFILING_TYPE(0, 0),
+    /**
+     * <code>VTS_PROFILING_TYPE_TIMESTAMP = 1;</code>
+     *
+     * <pre>
+     * for one sample which measures the time between two profiling points.
+     * </pre>
+     */
+    VTS_PROFILING_TYPE_TIMESTAMP(1, 1),
+    /**
+     * <code>VTS_PROFILING_TYPE_LABELED_VECTOR = 2;</code>
+     *
+     * <pre>
+     * for multiple single-type samples with labels.
+     * </pre>
+     */
+    VTS_PROFILING_TYPE_LABELED_VECTOR(2, 2),
+    /**
+     * <code>VTS_PROFILING_TYPE_UNLABELED_VECTOR = 3;</code>
+     *
+     * <pre>
+     * for multiple single-type samples without labels.
+     * </pre>
+     */
+    VTS_PROFILING_TYPE_UNLABELED_VECTOR(3, 3),
+    ;
+
+    /**
+     * <code>UNKNOWN_VTS_PROFILING_TYPE = 0;</code>
+     */
+    public static final int UNKNOWN_VTS_PROFILING_TYPE_VALUE = 0;
+    /**
+     * <code>VTS_PROFILING_TYPE_TIMESTAMP = 1;</code>
+     *
+     * <pre>
+     * for one sample which measures the time between two profiling points.
+     * </pre>
+     */
+    public static final int VTS_PROFILING_TYPE_TIMESTAMP_VALUE = 1;
+    /**
+     * <code>VTS_PROFILING_TYPE_LABELED_VECTOR = 2;</code>
+     *
+     * <pre>
+     * for multiple single-type samples with labels.
+     * </pre>
+     */
+    public static final int VTS_PROFILING_TYPE_LABELED_VECTOR_VALUE = 2;
+    /**
+     * <code>VTS_PROFILING_TYPE_UNLABELED_VECTOR = 3;</code>
+     *
+     * <pre>
+     * for multiple single-type samples without labels.
+     * </pre>
+     */
+    public static final int VTS_PROFILING_TYPE_UNLABELED_VECTOR_VALUE = 3;
+
+
+    public final int getNumber() { return value; }
+
+    public static VtsProfilingType valueOf(int value) {
+      switch (value) {
+        case 0: return UNKNOWN_VTS_PROFILING_TYPE;
+        case 1: return VTS_PROFILING_TYPE_TIMESTAMP;
+        case 2: return VTS_PROFILING_TYPE_LABELED_VECTOR;
+        case 3: return VTS_PROFILING_TYPE_UNLABELED_VECTOR;
+        default: return null;
+      }
+    }
+
+    public static com.google.protobuf.Internal.EnumLiteMap<VtsProfilingType>
+        internalGetValueMap() {
+      return internalValueMap;
+    }
+    private static com.google.protobuf.Internal.EnumLiteMap<VtsProfilingType>
+        internalValueMap =
+          new com.google.protobuf.Internal.EnumLiteMap<VtsProfilingType>() {
+            public VtsProfilingType findValueByNumber(int number) {
+              return VtsProfilingType.valueOf(number);
+            }
+          };
+
+    public final com.google.protobuf.Descriptors.EnumValueDescriptor
+        getValueDescriptor() {
+      return getDescriptor().getValues().get(index);
+    }
+    public final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptorForType() {
+      return getDescriptor();
+    }
+    public static final com.google.protobuf.Descriptors.EnumDescriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.getDescriptor().getEnumTypes().get(3);
+    }
+
+    private static final VtsProfilingType[] VALUES = values();
+
+    public static VtsProfilingType valueOf(
+        com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+      if (desc.getType() != getDescriptor()) {
+        throw new java.lang.IllegalArgumentException(
+          "EnumValueDescriptor is not for this type.");
+      }
+      return VALUES[desc.getIndex()];
+    }
+
+    private final int index;
+    private final int value;
+
+    private VtsProfilingType(int index, int value) {
+      this.index = index;
+      this.value = value;
+    }
+
+    // @@protoc_insertion_point(enum_scope:android.vts.VtsProfilingType)
+  }
+
+  public interface AndroidDeviceInfoMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes product_type = 1;
+    /**
+     * <code>optional bytes product_type = 1;</code>
+     *
+     * <pre>
+     * product type (e.g., bullhead).
+     * </pre>
+     */
+    boolean hasProductType();
+    /**
+     * <code>optional bytes product_type = 1;</code>
+     *
+     * <pre>
+     * product type (e.g., bullhead).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getProductType();
+
+    // optional bytes product_variant = 2;
+    /**
+     * <code>optional bytes product_variant = 2;</code>
+     *
+     * <pre>
+     * product type variant (e.g., still bullhead or another name).
+     * </pre>
+     */
+    boolean hasProductVariant();
+    /**
+     * <code>optional bytes product_variant = 2;</code>
+     *
+     * <pre>
+     * product type variant (e.g., still bullhead or another name).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getProductVariant();
+
+    // optional bytes build_flavor = 11;
+    /**
+     * <code>optional bytes build_flavor = 11;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug).
+     * </pre>
+     */
+    boolean hasBuildFlavor();
+    /**
+     * <code>optional bytes build_flavor = 11;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBuildFlavor();
+
+    // optional bytes build_id = 12;
+    /**
+     * <code>optional bytes build_id = 12;</code>
+     *
+     * <pre>
+     * Android Build ID.
+     * </pre>
+     */
+    boolean hasBuildId();
+    /**
+     * <code>optional bytes build_id = 12;</code>
+     *
+     * <pre>
+     * Android Build ID.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBuildId();
+
+    // optional bytes branch = 21;
+    /**
+     * <code>optional bytes branch = 21;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev).
+     * </pre>
+     */
+    boolean hasBranch();
+    /**
+     * <code>optional bytes branch = 21;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBranch();
+
+    // optional bytes build_alias = 22;
+    /**
+     * <code>optional bytes build_alias = 22;</code>
+     *
+     * <pre>
+     * build alias implies the branch name.
+     * </pre>
+     */
+    boolean hasBuildAlias();
+    /**
+     * <code>optional bytes build_alias = 22;</code>
+     *
+     * <pre>
+     * build alias implies the branch name.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBuildAlias();
+
+    // optional bytes api_level = 31;
+    /**
+     * <code>optional bytes api_level = 31;</code>
+     *
+     * <pre>
+     * API level
+     * </pre>
+     */
+    boolean hasApiLevel();
+    /**
+     * <code>optional bytes api_level = 31;</code>
+     *
+     * <pre>
+     * API level
+     * </pre>
+     */
+    com.google.protobuf.ByteString getApiLevel();
+
+    // optional bytes abi_name = 51;
+    /**
+     * <code>optional bytes abi_name = 51;</code>
+     *
+     * <pre>
+     * ABI name that is current in use for the test
+     * </pre>
+     */
+    boolean hasAbiName();
+    /**
+     * <code>optional bytes abi_name = 51;</code>
+     *
+     * <pre>
+     * ABI name that is current in use for the test
+     * </pre>
+     */
+    com.google.protobuf.ByteString getAbiName();
+
+    // optional bytes abi_bitness = 52;
+    /**
+     * <code>optional bytes abi_bitness = 52;</code>
+     *
+     * <pre>
+     * ABI bitness that is current in use for the test. Example: '32', '64',
+     * </pre>
+     */
+    boolean hasAbiBitness();
+    /**
+     * <code>optional bytes abi_bitness = 52;</code>
+     *
+     * <pre>
+     * ABI bitness that is current in use for the test. Example: '32', '64',
+     * </pre>
+     */
+    com.google.protobuf.ByteString getAbiBitness();
+
+    // optional bytes serial = 101;
+    /**
+     * <code>optional bytes serial = 101;</code>
+     *
+     * <pre>
+     * Device USB serial number
+     * </pre>
+     */
+    boolean hasSerial();
+    /**
+     * <code>optional bytes serial = 101;</code>
+     *
+     * <pre>
+     * Device USB serial number
+     * </pre>
+     */
+    com.google.protobuf.ByteString getSerial();
+  }
+  /**
+   * Protobuf type {@code android.vts.AndroidDeviceInfoMessage}
+   *
+   * <pre>
+   * To specify a call flow event.
+   * </pre>
+   */
+  public static final class AndroidDeviceInfoMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements AndroidDeviceInfoMessageOrBuilder {
+    // Use AndroidDeviceInfoMessage.newBuilder() to construct.
+    private AndroidDeviceInfoMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private AndroidDeviceInfoMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final AndroidDeviceInfoMessage defaultInstance;
+    public static AndroidDeviceInfoMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public AndroidDeviceInfoMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private AndroidDeviceInfoMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              productType_ = input.readBytes();
+              break;
+            }
+            case 18: {
+              bitField0_ |= 0x00000002;
+              productVariant_ = input.readBytes();
+              break;
+            }
+            case 90: {
+              bitField0_ |= 0x00000004;
+              buildFlavor_ = input.readBytes();
+              break;
+            }
+            case 98: {
+              bitField0_ |= 0x00000008;
+              buildId_ = input.readBytes();
+              break;
+            }
+            case 170: {
+              bitField0_ |= 0x00000010;
+              branch_ = input.readBytes();
+              break;
+            }
+            case 178: {
+              bitField0_ |= 0x00000020;
+              buildAlias_ = input.readBytes();
+              break;
+            }
+            case 250: {
+              bitField0_ |= 0x00000040;
+              apiLevel_ = input.readBytes();
+              break;
+            }
+            case 410: {
+              bitField0_ |= 0x00000080;
+              abiName_ = input.readBytes();
+              break;
+            }
+            case 418: {
+              bitField0_ |= 0x00000100;
+              abiBitness_ = input.readBytes();
+              break;
+            }
+            case 810: {
+              bitField0_ |= 0x00000200;
+              serial_ = input.readBytes();
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidDeviceInfoMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidDeviceInfoMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.class, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<AndroidDeviceInfoMessage> PARSER =
+        new com.google.protobuf.AbstractParser<AndroidDeviceInfoMessage>() {
+      public AndroidDeviceInfoMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new AndroidDeviceInfoMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<AndroidDeviceInfoMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes product_type = 1;
+    public static final int PRODUCT_TYPE_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString productType_;
+    /**
+     * <code>optional bytes product_type = 1;</code>
+     *
+     * <pre>
+     * product type (e.g., bullhead).
+     * </pre>
+     */
+    public boolean hasProductType() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes product_type = 1;</code>
+     *
+     * <pre>
+     * product type (e.g., bullhead).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getProductType() {
+      return productType_;
+    }
+
+    // optional bytes product_variant = 2;
+    public static final int PRODUCT_VARIANT_FIELD_NUMBER = 2;
+    private com.google.protobuf.ByteString productVariant_;
+    /**
+     * <code>optional bytes product_variant = 2;</code>
+     *
+     * <pre>
+     * product type variant (e.g., still bullhead or another name).
+     * </pre>
+     */
+    public boolean hasProductVariant() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional bytes product_variant = 2;</code>
+     *
+     * <pre>
+     * product type variant (e.g., still bullhead or another name).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getProductVariant() {
+      return productVariant_;
+    }
+
+    // optional bytes build_flavor = 11;
+    public static final int BUILD_FLAVOR_FIELD_NUMBER = 11;
+    private com.google.protobuf.ByteString buildFlavor_;
+    /**
+     * <code>optional bytes build_flavor = 11;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug).
+     * </pre>
+     */
+    public boolean hasBuildFlavor() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional bytes build_flavor = 11;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBuildFlavor() {
+      return buildFlavor_;
+    }
+
+    // optional bytes build_id = 12;
+    public static final int BUILD_ID_FIELD_NUMBER = 12;
+    private com.google.protobuf.ByteString buildId_;
+    /**
+     * <code>optional bytes build_id = 12;</code>
+     *
+     * <pre>
+     * Android Build ID.
+     * </pre>
+     */
+    public boolean hasBuildId() {
+      return ((bitField0_ & 0x00000008) == 0x00000008);
+    }
+    /**
+     * <code>optional bytes build_id = 12;</code>
+     *
+     * <pre>
+     * Android Build ID.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBuildId() {
+      return buildId_;
+    }
+
+    // optional bytes branch = 21;
+    public static final int BRANCH_FIELD_NUMBER = 21;
+    private com.google.protobuf.ByteString branch_;
+    /**
+     * <code>optional bytes branch = 21;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev).
+     * </pre>
+     */
+    public boolean hasBranch() {
+      return ((bitField0_ & 0x00000010) == 0x00000010);
+    }
+    /**
+     * <code>optional bytes branch = 21;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBranch() {
+      return branch_;
+    }
+
+    // optional bytes build_alias = 22;
+    public static final int BUILD_ALIAS_FIELD_NUMBER = 22;
+    private com.google.protobuf.ByteString buildAlias_;
+    /**
+     * <code>optional bytes build_alias = 22;</code>
+     *
+     * <pre>
+     * build alias implies the branch name.
+     * </pre>
+     */
+    public boolean hasBuildAlias() {
+      return ((bitField0_ & 0x00000020) == 0x00000020);
+    }
+    /**
+     * <code>optional bytes build_alias = 22;</code>
+     *
+     * <pre>
+     * build alias implies the branch name.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBuildAlias() {
+      return buildAlias_;
+    }
+
+    // optional bytes api_level = 31;
+    public static final int API_LEVEL_FIELD_NUMBER = 31;
+    private com.google.protobuf.ByteString apiLevel_;
+    /**
+     * <code>optional bytes api_level = 31;</code>
+     *
+     * <pre>
+     * API level
+     * </pre>
+     */
+    public boolean hasApiLevel() {
+      return ((bitField0_ & 0x00000040) == 0x00000040);
+    }
+    /**
+     * <code>optional bytes api_level = 31;</code>
+     *
+     * <pre>
+     * API level
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getApiLevel() {
+      return apiLevel_;
+    }
+
+    // optional bytes abi_name = 51;
+    public static final int ABI_NAME_FIELD_NUMBER = 51;
+    private com.google.protobuf.ByteString abiName_;
+    /**
+     * <code>optional bytes abi_name = 51;</code>
+     *
+     * <pre>
+     * ABI name that is current in use for the test
+     * </pre>
+     */
+    public boolean hasAbiName() {
+      return ((bitField0_ & 0x00000080) == 0x00000080);
+    }
+    /**
+     * <code>optional bytes abi_name = 51;</code>
+     *
+     * <pre>
+     * ABI name that is current in use for the test
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getAbiName() {
+      return abiName_;
+    }
+
+    // optional bytes abi_bitness = 52;
+    public static final int ABI_BITNESS_FIELD_NUMBER = 52;
+    private com.google.protobuf.ByteString abiBitness_;
+    /**
+     * <code>optional bytes abi_bitness = 52;</code>
+     *
+     * <pre>
+     * ABI bitness that is current in use for the test. Example: '32', '64',
+     * </pre>
+     */
+    public boolean hasAbiBitness() {
+      return ((bitField0_ & 0x00000100) == 0x00000100);
+    }
+    /**
+     * <code>optional bytes abi_bitness = 52;</code>
+     *
+     * <pre>
+     * ABI bitness that is current in use for the test. Example: '32', '64',
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getAbiBitness() {
+      return abiBitness_;
+    }
+
+    // optional bytes serial = 101;
+    public static final int SERIAL_FIELD_NUMBER = 101;
+    private com.google.protobuf.ByteString serial_;
+    /**
+     * <code>optional bytes serial = 101;</code>
+     *
+     * <pre>
+     * Device USB serial number
+     * </pre>
+     */
+    public boolean hasSerial() {
+      return ((bitField0_ & 0x00000200) == 0x00000200);
+    }
+    /**
+     * <code>optional bytes serial = 101;</code>
+     *
+     * <pre>
+     * Device USB serial number
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getSerial() {
+      return serial_;
+    }
+
+    private void initFields() {
+      productType_ = com.google.protobuf.ByteString.EMPTY;
+      productVariant_ = com.google.protobuf.ByteString.EMPTY;
+      buildFlavor_ = com.google.protobuf.ByteString.EMPTY;
+      buildId_ = com.google.protobuf.ByteString.EMPTY;
+      branch_ = com.google.protobuf.ByteString.EMPTY;
+      buildAlias_ = com.google.protobuf.ByteString.EMPTY;
+      apiLevel_ = com.google.protobuf.ByteString.EMPTY;
+      abiName_ = com.google.protobuf.ByteString.EMPTY;
+      abiBitness_ = com.google.protobuf.ByteString.EMPTY;
+      serial_ = com.google.protobuf.ByteString.EMPTY;
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, productType_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeBytes(2, productVariant_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeBytes(11, buildFlavor_);
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        output.writeBytes(12, buildId_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        output.writeBytes(21, branch_);
+      }
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        output.writeBytes(22, buildAlias_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        output.writeBytes(31, apiLevel_);
+      }
+      if (((bitField0_ & 0x00000080) == 0x00000080)) {
+        output.writeBytes(51, abiName_);
+      }
+      if (((bitField0_ & 0x00000100) == 0x00000100)) {
+        output.writeBytes(52, abiBitness_);
+      }
+      if (((bitField0_ & 0x00000200) == 0x00000200)) {
+        output.writeBytes(101, serial_);
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, productType_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(2, productVariant_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(11, buildFlavor_);
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(12, buildId_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(21, branch_);
+      }
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(22, buildAlias_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(31, apiLevel_);
+      }
+      if (((bitField0_ & 0x00000080) == 0x00000080)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(51, abiName_);
+      }
+      if (((bitField0_ & 0x00000100) == 0x00000100)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(52, abiBitness_);
+      }
+      if (((bitField0_ & 0x00000200) == 0x00000200)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(101, serial_);
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.AndroidDeviceInfoMessage}
+     *
+     * <pre>
+     * To specify a call flow event.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidDeviceInfoMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidDeviceInfoMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.class, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        productType_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        productVariant_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        buildFlavor_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        buildId_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000008);
+        branch_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000010);
+        buildAlias_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000020);
+        apiLevel_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000040);
+        abiName_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000080);
+        abiBitness_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000100);
+        serial_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000200);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidDeviceInfoMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage build() {
+        com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage result = new com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.productType_ = productType_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.productVariant_ = productVariant_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.buildFlavor_ = buildFlavor_;
+        if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
+          to_bitField0_ |= 0x00000008;
+        }
+        result.buildId_ = buildId_;
+        if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
+          to_bitField0_ |= 0x00000010;
+        }
+        result.branch_ = branch_;
+        if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
+          to_bitField0_ |= 0x00000020;
+        }
+        result.buildAlias_ = buildAlias_;
+        if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
+          to_bitField0_ |= 0x00000040;
+        }
+        result.apiLevel_ = apiLevel_;
+        if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
+          to_bitField0_ |= 0x00000080;
+        }
+        result.abiName_ = abiName_;
+        if (((from_bitField0_ & 0x00000100) == 0x00000100)) {
+          to_bitField0_ |= 0x00000100;
+        }
+        result.abiBitness_ = abiBitness_;
+        if (((from_bitField0_ & 0x00000200) == 0x00000200)) {
+          to_bitField0_ |= 0x00000200;
+        }
+        result.serial_ = serial_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.getDefaultInstance()) return this;
+        if (other.hasProductType()) {
+          setProductType(other.getProductType());
+        }
+        if (other.hasProductVariant()) {
+          setProductVariant(other.getProductVariant());
+        }
+        if (other.hasBuildFlavor()) {
+          setBuildFlavor(other.getBuildFlavor());
+        }
+        if (other.hasBuildId()) {
+          setBuildId(other.getBuildId());
+        }
+        if (other.hasBranch()) {
+          setBranch(other.getBranch());
+        }
+        if (other.hasBuildAlias()) {
+          setBuildAlias(other.getBuildAlias());
+        }
+        if (other.hasApiLevel()) {
+          setApiLevel(other.getApiLevel());
+        }
+        if (other.hasAbiName()) {
+          setAbiName(other.getAbiName());
+        }
+        if (other.hasAbiBitness()) {
+          setAbiBitness(other.getAbiBitness());
+        }
+        if (other.hasSerial()) {
+          setSerial(other.getSerial());
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes product_type = 1;
+      private com.google.protobuf.ByteString productType_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes product_type = 1;</code>
+       *
+       * <pre>
+       * product type (e.g., bullhead).
+       * </pre>
+       */
+      public boolean hasProductType() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes product_type = 1;</code>
+       *
+       * <pre>
+       * product type (e.g., bullhead).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getProductType() {
+        return productType_;
+      }
+      /**
+       * <code>optional bytes product_type = 1;</code>
+       *
+       * <pre>
+       * product type (e.g., bullhead).
+       * </pre>
+       */
+      public Builder setProductType(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        productType_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes product_type = 1;</code>
+       *
+       * <pre>
+       * product type (e.g., bullhead).
+       * </pre>
+       */
+      public Builder clearProductType() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        productType_ = getDefaultInstance().getProductType();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes product_variant = 2;
+      private com.google.protobuf.ByteString productVariant_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes product_variant = 2;</code>
+       *
+       * <pre>
+       * product type variant (e.g., still bullhead or another name).
+       * </pre>
+       */
+      public boolean hasProductVariant() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional bytes product_variant = 2;</code>
+       *
+       * <pre>
+       * product type variant (e.g., still bullhead or another name).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getProductVariant() {
+        return productVariant_;
+      }
+      /**
+       * <code>optional bytes product_variant = 2;</code>
+       *
+       * <pre>
+       * product type variant (e.g., still bullhead or another name).
+       * </pre>
+       */
+      public Builder setProductVariant(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000002;
+        productVariant_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes product_variant = 2;</code>
+       *
+       * <pre>
+       * product type variant (e.g., still bullhead or another name).
+       * </pre>
+       */
+      public Builder clearProductVariant() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        productVariant_ = getDefaultInstance().getProductVariant();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes build_flavor = 11;
+      private com.google.protobuf.ByteString buildFlavor_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes build_flavor = 11;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug).
+       * </pre>
+       */
+      public boolean hasBuildFlavor() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional bytes build_flavor = 11;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBuildFlavor() {
+        return buildFlavor_;
+      }
+      /**
+       * <code>optional bytes build_flavor = 11;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug).
+       * </pre>
+       */
+      public Builder setBuildFlavor(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        buildFlavor_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes build_flavor = 11;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug).
+       * </pre>
+       */
+      public Builder clearBuildFlavor() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        buildFlavor_ = getDefaultInstance().getBuildFlavor();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes build_id = 12;
+      private com.google.protobuf.ByteString buildId_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes build_id = 12;</code>
+       *
+       * <pre>
+       * Android Build ID.
+       * </pre>
+       */
+      public boolean hasBuildId() {
+        return ((bitField0_ & 0x00000008) == 0x00000008);
+      }
+      /**
+       * <code>optional bytes build_id = 12;</code>
+       *
+       * <pre>
+       * Android Build ID.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBuildId() {
+        return buildId_;
+      }
+      /**
+       * <code>optional bytes build_id = 12;</code>
+       *
+       * <pre>
+       * Android Build ID.
+       * </pre>
+       */
+      public Builder setBuildId(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000008;
+        buildId_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes build_id = 12;</code>
+       *
+       * <pre>
+       * Android Build ID.
+       * </pre>
+       */
+      public Builder clearBuildId() {
+        bitField0_ = (bitField0_ & ~0x00000008);
+        buildId_ = getDefaultInstance().getBuildId();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes branch = 21;
+      private com.google.protobuf.ByteString branch_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes branch = 21;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev).
+       * </pre>
+       */
+      public boolean hasBranch() {
+        return ((bitField0_ & 0x00000010) == 0x00000010);
+      }
+      /**
+       * <code>optional bytes branch = 21;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBranch() {
+        return branch_;
+      }
+      /**
+       * <code>optional bytes branch = 21;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev).
+       * </pre>
+       */
+      public Builder setBranch(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000010;
+        branch_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes branch = 21;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev).
+       * </pre>
+       */
+      public Builder clearBranch() {
+        bitField0_ = (bitField0_ & ~0x00000010);
+        branch_ = getDefaultInstance().getBranch();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes build_alias = 22;
+      private com.google.protobuf.ByteString buildAlias_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes build_alias = 22;</code>
+       *
+       * <pre>
+       * build alias implies the branch name.
+       * </pre>
+       */
+      public boolean hasBuildAlias() {
+        return ((bitField0_ & 0x00000020) == 0x00000020);
+      }
+      /**
+       * <code>optional bytes build_alias = 22;</code>
+       *
+       * <pre>
+       * build alias implies the branch name.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBuildAlias() {
+        return buildAlias_;
+      }
+      /**
+       * <code>optional bytes build_alias = 22;</code>
+       *
+       * <pre>
+       * build alias implies the branch name.
+       * </pre>
+       */
+      public Builder setBuildAlias(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000020;
+        buildAlias_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes build_alias = 22;</code>
+       *
+       * <pre>
+       * build alias implies the branch name.
+       * </pre>
+       */
+      public Builder clearBuildAlias() {
+        bitField0_ = (bitField0_ & ~0x00000020);
+        buildAlias_ = getDefaultInstance().getBuildAlias();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes api_level = 31;
+      private com.google.protobuf.ByteString apiLevel_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes api_level = 31;</code>
+       *
+       * <pre>
+       * API level
+       * </pre>
+       */
+      public boolean hasApiLevel() {
+        return ((bitField0_ & 0x00000040) == 0x00000040);
+      }
+      /**
+       * <code>optional bytes api_level = 31;</code>
+       *
+       * <pre>
+       * API level
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getApiLevel() {
+        return apiLevel_;
+      }
+      /**
+       * <code>optional bytes api_level = 31;</code>
+       *
+       * <pre>
+       * API level
+       * </pre>
+       */
+      public Builder setApiLevel(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000040;
+        apiLevel_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes api_level = 31;</code>
+       *
+       * <pre>
+       * API level
+       * </pre>
+       */
+      public Builder clearApiLevel() {
+        bitField0_ = (bitField0_ & ~0x00000040);
+        apiLevel_ = getDefaultInstance().getApiLevel();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes abi_name = 51;
+      private com.google.protobuf.ByteString abiName_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes abi_name = 51;</code>
+       *
+       * <pre>
+       * ABI name that is current in use for the test
+       * </pre>
+       */
+      public boolean hasAbiName() {
+        return ((bitField0_ & 0x00000080) == 0x00000080);
+      }
+      /**
+       * <code>optional bytes abi_name = 51;</code>
+       *
+       * <pre>
+       * ABI name that is current in use for the test
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getAbiName() {
+        return abiName_;
+      }
+      /**
+       * <code>optional bytes abi_name = 51;</code>
+       *
+       * <pre>
+       * ABI name that is current in use for the test
+       * </pre>
+       */
+      public Builder setAbiName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000080;
+        abiName_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes abi_name = 51;</code>
+       *
+       * <pre>
+       * ABI name that is current in use for the test
+       * </pre>
+       */
+      public Builder clearAbiName() {
+        bitField0_ = (bitField0_ & ~0x00000080);
+        abiName_ = getDefaultInstance().getAbiName();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes abi_bitness = 52;
+      private com.google.protobuf.ByteString abiBitness_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes abi_bitness = 52;</code>
+       *
+       * <pre>
+       * ABI bitness that is current in use for the test. Example: '32', '64',
+       * </pre>
+       */
+      public boolean hasAbiBitness() {
+        return ((bitField0_ & 0x00000100) == 0x00000100);
+      }
+      /**
+       * <code>optional bytes abi_bitness = 52;</code>
+       *
+       * <pre>
+       * ABI bitness that is current in use for the test. Example: '32', '64',
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getAbiBitness() {
+        return abiBitness_;
+      }
+      /**
+       * <code>optional bytes abi_bitness = 52;</code>
+       *
+       * <pre>
+       * ABI bitness that is current in use for the test. Example: '32', '64',
+       * </pre>
+       */
+      public Builder setAbiBitness(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000100;
+        abiBitness_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes abi_bitness = 52;</code>
+       *
+       * <pre>
+       * ABI bitness that is current in use for the test. Example: '32', '64',
+       * </pre>
+       */
+      public Builder clearAbiBitness() {
+        bitField0_ = (bitField0_ & ~0x00000100);
+        abiBitness_ = getDefaultInstance().getAbiBitness();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes serial = 101;
+      private com.google.protobuf.ByteString serial_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes serial = 101;</code>
+       *
+       * <pre>
+       * Device USB serial number
+       * </pre>
+       */
+      public boolean hasSerial() {
+        return ((bitField0_ & 0x00000200) == 0x00000200);
+      }
+      /**
+       * <code>optional bytes serial = 101;</code>
+       *
+       * <pre>
+       * Device USB serial number
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getSerial() {
+        return serial_;
+      }
+      /**
+       * <code>optional bytes serial = 101;</code>
+       *
+       * <pre>
+       * Device USB serial number
+       * </pre>
+       */
+      public Builder setSerial(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000200;
+        serial_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes serial = 101;</code>
+       *
+       * <pre>
+       * Device USB serial number
+       * </pre>
+       */
+      public Builder clearSerial() {
+        bitField0_ = (bitField0_ & ~0x00000200);
+        serial_ = getDefaultInstance().getSerial();
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.AndroidDeviceInfoMessage)
+    }
+
+    static {
+      defaultInstance = new AndroidDeviceInfoMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.AndroidDeviceInfoMessage)
+  }
+
+  public interface AndroidBuildInfoOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes id = 1;
+    /**
+     * <code>optional bytes id = 1;</code>
+     *
+     * <pre>
+     * build ID.
+     * </pre>
+     */
+    boolean hasId();
+    /**
+     * <code>optional bytes id = 1;</code>
+     *
+     * <pre>
+     * build ID.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getId();
+
+    // optional bytes name = 11;
+    /**
+     * <code>optional bytes name = 11;</code>
+     *
+     * <pre>
+     * device name (e.g., bullhead).
+     * </pre>
+     */
+    boolean hasName();
+    /**
+     * <code>optional bytes name = 11;</code>
+     *
+     * <pre>
+     * device name (e.g., bullhead).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getName();
+
+    // optional bytes build_type = 12;
+    /**
+     * <code>optional bytes build_type = 12;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug)
+     * </pre>
+     */
+    boolean hasBuildType();
+    /**
+     * <code>optional bytes build_type = 12;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug)
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBuildType();
+
+    // optional bytes branch = 13;
+    /**
+     * <code>optional bytes branch = 13;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev)
+     * </pre>
+     */
+    boolean hasBranch();
+    /**
+     * <code>optional bytes branch = 13;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev)
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBranch();
+
+    // optional bytes build_summary = 21;
+    /**
+     * <code>optional bytes build_summary = 21;</code>
+     *
+     * <pre>
+     * indicates the latest commit information of each branch (e.g., xml format).
+     * </pre>
+     */
+    boolean hasBuildSummary();
+    /**
+     * <code>optional bytes build_summary = 21;</code>
+     *
+     * <pre>
+     * indicates the latest commit information of each branch (e.g., xml format).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getBuildSummary();
+  }
+  /**
+   * Protobuf type {@code android.vts.AndroidBuildInfo}
+   *
+   * <pre>
+   * To specify build info.
+   * </pre>
+   */
+  public static final class AndroidBuildInfo extends
+      com.google.protobuf.GeneratedMessage
+      implements AndroidBuildInfoOrBuilder {
+    // Use AndroidBuildInfo.newBuilder() to construct.
+    private AndroidBuildInfo(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private AndroidBuildInfo(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final AndroidBuildInfo defaultInstance;
+    public static AndroidBuildInfo getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public AndroidBuildInfo getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private AndroidBuildInfo(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              id_ = input.readBytes();
+              break;
+            }
+            case 90: {
+              bitField0_ |= 0x00000002;
+              name_ = input.readBytes();
+              break;
+            }
+            case 98: {
+              bitField0_ |= 0x00000004;
+              buildType_ = input.readBytes();
+              break;
+            }
+            case 106: {
+              bitField0_ |= 0x00000008;
+              branch_ = input.readBytes();
+              break;
+            }
+            case 170: {
+              bitField0_ |= 0x00000010;
+              buildSummary_ = input.readBytes();
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidBuildInfo_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidBuildInfo_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.class, com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<AndroidBuildInfo> PARSER =
+        new com.google.protobuf.AbstractParser<AndroidBuildInfo>() {
+      public AndroidBuildInfo parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new AndroidBuildInfo(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<AndroidBuildInfo> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes id = 1;
+    public static final int ID_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString id_;
+    /**
+     * <code>optional bytes id = 1;</code>
+     *
+     * <pre>
+     * build ID.
+     * </pre>
+     */
+    public boolean hasId() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes id = 1;</code>
+     *
+     * <pre>
+     * build ID.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getId() {
+      return id_;
+    }
+
+    // optional bytes name = 11;
+    public static final int NAME_FIELD_NUMBER = 11;
+    private com.google.protobuf.ByteString name_;
+    /**
+     * <code>optional bytes name = 11;</code>
+     *
+     * <pre>
+     * device name (e.g., bullhead).
+     * </pre>
+     */
+    public boolean hasName() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional bytes name = 11;</code>
+     *
+     * <pre>
+     * device name (e.g., bullhead).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getName() {
+      return name_;
+    }
+
+    // optional bytes build_type = 12;
+    public static final int BUILD_TYPE_FIELD_NUMBER = 12;
+    private com.google.protobuf.ByteString buildType_;
+    /**
+     * <code>optional bytes build_type = 12;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug)
+     * </pre>
+     */
+    public boolean hasBuildType() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional bytes build_type = 12;</code>
+     *
+     * <pre>
+     * build type (e.g., userdebug)
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBuildType() {
+      return buildType_;
+    }
+
+    // optional bytes branch = 13;
+    public static final int BRANCH_FIELD_NUMBER = 13;
+    private com.google.protobuf.ByteString branch_;
+    /**
+     * <code>optional bytes branch = 13;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev)
+     * </pre>
+     */
+    public boolean hasBranch() {
+      return ((bitField0_ & 0x00000008) == 0x00000008);
+    }
+    /**
+     * <code>optional bytes branch = 13;</code>
+     *
+     * <pre>
+     * branch name (e.g., master or nyc-dev)
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBranch() {
+      return branch_;
+    }
+
+    // optional bytes build_summary = 21;
+    public static final int BUILD_SUMMARY_FIELD_NUMBER = 21;
+    private com.google.protobuf.ByteString buildSummary_;
+    /**
+     * <code>optional bytes build_summary = 21;</code>
+     *
+     * <pre>
+     * indicates the latest commit information of each branch (e.g., xml format).
+     * </pre>
+     */
+    public boolean hasBuildSummary() {
+      return ((bitField0_ & 0x00000010) == 0x00000010);
+    }
+    /**
+     * <code>optional bytes build_summary = 21;</code>
+     *
+     * <pre>
+     * indicates the latest commit information of each branch (e.g., xml format).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getBuildSummary() {
+      return buildSummary_;
+    }
+
+    private void initFields() {
+      id_ = com.google.protobuf.ByteString.EMPTY;
+      name_ = com.google.protobuf.ByteString.EMPTY;
+      buildType_ = com.google.protobuf.ByteString.EMPTY;
+      branch_ = com.google.protobuf.ByteString.EMPTY;
+      buildSummary_ = com.google.protobuf.ByteString.EMPTY;
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, id_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeBytes(11, name_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeBytes(12, buildType_);
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        output.writeBytes(13, branch_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        output.writeBytes(21, buildSummary_);
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, id_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(11, name_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(12, buildType_);
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(13, branch_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(21, buildSummary_);
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.AndroidBuildInfo prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.AndroidBuildInfo}
+     *
+     * <pre>
+     * To specify build info.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidBuildInfo_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidBuildInfo_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.class, com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        id_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        name_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        buildType_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        branch_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000008);
+        buildSummary_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000010);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_AndroidBuildInfo_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.AndroidBuildInfo getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.AndroidBuildInfo build() {
+        com.android.vts.proto.VtsReportMessage.AndroidBuildInfo result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.AndroidBuildInfo buildPartial() {
+        com.android.vts.proto.VtsReportMessage.AndroidBuildInfo result = new com.android.vts.proto.VtsReportMessage.AndroidBuildInfo(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.id_ = id_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.name_ = name_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.buildType_ = buildType_;
+        if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
+          to_bitField0_ |= 0x00000008;
+        }
+        result.branch_ = branch_;
+        if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
+          to_bitField0_ |= 0x00000010;
+        }
+        result.buildSummary_ = buildSummary_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.AndroidBuildInfo) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.AndroidBuildInfo)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.AndroidBuildInfo other) {
+        if (other == com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance()) return this;
+        if (other.hasId()) {
+          setId(other.getId());
+        }
+        if (other.hasName()) {
+          setName(other.getName());
+        }
+        if (other.hasBuildType()) {
+          setBuildType(other.getBuildType());
+        }
+        if (other.hasBranch()) {
+          setBranch(other.getBranch());
+        }
+        if (other.hasBuildSummary()) {
+          setBuildSummary(other.getBuildSummary());
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.AndroidBuildInfo parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.AndroidBuildInfo) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes id = 1;
+      private com.google.protobuf.ByteString id_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes id = 1;</code>
+       *
+       * <pre>
+       * build ID.
+       * </pre>
+       */
+      public boolean hasId() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes id = 1;</code>
+       *
+       * <pre>
+       * build ID.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getId() {
+        return id_;
+      }
+      /**
+       * <code>optional bytes id = 1;</code>
+       *
+       * <pre>
+       * build ID.
+       * </pre>
+       */
+      public Builder setId(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        id_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes id = 1;</code>
+       *
+       * <pre>
+       * build ID.
+       * </pre>
+       */
+      public Builder clearId() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        id_ = getDefaultInstance().getId();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes name = 11;
+      private com.google.protobuf.ByteString name_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes name = 11;</code>
+       *
+       * <pre>
+       * device name (e.g., bullhead).
+       * </pre>
+       */
+      public boolean hasName() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional bytes name = 11;</code>
+       *
+       * <pre>
+       * device name (e.g., bullhead).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getName() {
+        return name_;
+      }
+      /**
+       * <code>optional bytes name = 11;</code>
+       *
+       * <pre>
+       * device name (e.g., bullhead).
+       * </pre>
+       */
+      public Builder setName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000002;
+        name_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes name = 11;</code>
+       *
+       * <pre>
+       * device name (e.g., bullhead).
+       * </pre>
+       */
+      public Builder clearName() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        name_ = getDefaultInstance().getName();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes build_type = 12;
+      private com.google.protobuf.ByteString buildType_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes build_type = 12;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug)
+       * </pre>
+       */
+      public boolean hasBuildType() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional bytes build_type = 12;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug)
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBuildType() {
+        return buildType_;
+      }
+      /**
+       * <code>optional bytes build_type = 12;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug)
+       * </pre>
+       */
+      public Builder setBuildType(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        buildType_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes build_type = 12;</code>
+       *
+       * <pre>
+       * build type (e.g., userdebug)
+       * </pre>
+       */
+      public Builder clearBuildType() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        buildType_ = getDefaultInstance().getBuildType();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes branch = 13;
+      private com.google.protobuf.ByteString branch_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes branch = 13;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev)
+       * </pre>
+       */
+      public boolean hasBranch() {
+        return ((bitField0_ & 0x00000008) == 0x00000008);
+      }
+      /**
+       * <code>optional bytes branch = 13;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev)
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBranch() {
+        return branch_;
+      }
+      /**
+       * <code>optional bytes branch = 13;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev)
+       * </pre>
+       */
+      public Builder setBranch(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000008;
+        branch_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes branch = 13;</code>
+       *
+       * <pre>
+       * branch name (e.g., master or nyc-dev)
+       * </pre>
+       */
+      public Builder clearBranch() {
+        bitField0_ = (bitField0_ & ~0x00000008);
+        branch_ = getDefaultInstance().getBranch();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes build_summary = 21;
+      private com.google.protobuf.ByteString buildSummary_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes build_summary = 21;</code>
+       *
+       * <pre>
+       * indicates the latest commit information of each branch (e.g., xml format).
+       * </pre>
+       */
+      public boolean hasBuildSummary() {
+        return ((bitField0_ & 0x00000010) == 0x00000010);
+      }
+      /**
+       * <code>optional bytes build_summary = 21;</code>
+       *
+       * <pre>
+       * indicates the latest commit information of each branch (e.g., xml format).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getBuildSummary() {
+        return buildSummary_;
+      }
+      /**
+       * <code>optional bytes build_summary = 21;</code>
+       *
+       * <pre>
+       * indicates the latest commit information of each branch (e.g., xml format).
+       * </pre>
+       */
+      public Builder setBuildSummary(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000010;
+        buildSummary_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes build_summary = 21;</code>
+       *
+       * <pre>
+       * indicates the latest commit information of each branch (e.g., xml format).
+       * </pre>
+       */
+      public Builder clearBuildSummary() {
+        bitField0_ = (bitField0_ & ~0x00000010);
+        buildSummary_ = getDefaultInstance().getBuildSummary();
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.AndroidBuildInfo)
+    }
+
+    static {
+      defaultInstance = new AndroidBuildInfo(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.AndroidBuildInfo)
+  }
+
+  public interface VtsHostInfoOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes hostname = 1;
+    /**
+     * <code>optional bytes hostname = 1;</code>
+     *
+     * <pre>
+     * the host name (i.e., full domain name).
+     * </pre>
+     */
+    boolean hasHostname();
+    /**
+     * <code>optional bytes hostname = 1;</code>
+     *
+     * <pre>
+     * the host name (i.e., full domain name).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getHostname();
+  }
+  /**
+   * Protobuf type {@code android.vts.VtsHostInfo}
+   *
+   * <pre>
+   * To specify the information about a host node.
+   * </pre>
+   */
+  public static final class VtsHostInfo extends
+      com.google.protobuf.GeneratedMessage
+      implements VtsHostInfoOrBuilder {
+    // Use VtsHostInfo.newBuilder() to construct.
+    private VtsHostInfo(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private VtsHostInfo(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final VtsHostInfo defaultInstance;
+    public static VtsHostInfo getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public VtsHostInfo getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private VtsHostInfo(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              hostname_ = input.readBytes();
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_VtsHostInfo_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_VtsHostInfo_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.VtsHostInfo.class, com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<VtsHostInfo> PARSER =
+        new com.google.protobuf.AbstractParser<VtsHostInfo>() {
+      public VtsHostInfo parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new VtsHostInfo(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<VtsHostInfo> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes hostname = 1;
+    public static final int HOSTNAME_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString hostname_;
+    /**
+     * <code>optional bytes hostname = 1;</code>
+     *
+     * <pre>
+     * the host name (i.e., full domain name).
+     * </pre>
+     */
+    public boolean hasHostname() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes hostname = 1;</code>
+     *
+     * <pre>
+     * the host name (i.e., full domain name).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getHostname() {
+      return hostname_;
+    }
+
+    private void initFields() {
+      hostname_ = com.google.protobuf.ByteString.EMPTY;
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, hostname_);
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, hostname_);
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.VtsHostInfo parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.VtsHostInfo prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.VtsHostInfo}
+     *
+     * <pre>
+     * To specify the information about a host node.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_VtsHostInfo_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_VtsHostInfo_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.VtsHostInfo.class, com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.VtsHostInfo.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        hostname_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_VtsHostInfo_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.VtsHostInfo getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.VtsHostInfo build() {
+        com.android.vts.proto.VtsReportMessage.VtsHostInfo result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.VtsHostInfo buildPartial() {
+        com.android.vts.proto.VtsReportMessage.VtsHostInfo result = new com.android.vts.proto.VtsReportMessage.VtsHostInfo(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.hostname_ = hostname_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.VtsHostInfo) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.VtsHostInfo)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.VtsHostInfo other) {
+        if (other == com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance()) return this;
+        if (other.hasHostname()) {
+          setHostname(other.getHostname());
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.VtsHostInfo parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.VtsHostInfo) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes hostname = 1;
+      private com.google.protobuf.ByteString hostname_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes hostname = 1;</code>
+       *
+       * <pre>
+       * the host name (i.e., full domain name).
+       * </pre>
+       */
+      public boolean hasHostname() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes hostname = 1;</code>
+       *
+       * <pre>
+       * the host name (i.e., full domain name).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getHostname() {
+        return hostname_;
+      }
+      /**
+       * <code>optional bytes hostname = 1;</code>
+       *
+       * <pre>
+       * the host name (i.e., full domain name).
+       * </pre>
+       */
+      public Builder setHostname(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        hostname_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes hostname = 1;</code>
+       *
+       * <pre>
+       * the host name (i.e., full domain name).
+       * </pre>
+       */
+      public Builder clearHostname() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        hostname_ = getDefaultInstance().getHostname();
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.VtsHostInfo)
+    }
+
+    static {
+      defaultInstance = new VtsHostInfo(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.VtsHostInfo)
+  }
+
+  public interface TestCaseReportMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes name = 1;
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the test case name.
+     * </pre>
+     */
+    boolean hasName();
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the test case name.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getName();
+
+    // optional .android.vts.TestCaseResult test_result = 11;
+    /**
+     * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+     *
+     * <pre>
+     * the test result.
+     * </pre>
+     */
+    boolean hasTestResult();
+    /**
+     * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+     *
+     * <pre>
+     * the test result.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.TestCaseResult getTestResult();
+
+    // optional int64 start_timestamp = 21;
+    /**
+     * <code>optional int64 start_timestamp = 21;</code>
+     *
+     * <pre>
+     * execution start and end time stamp.
+     * </pre>
+     */
+    boolean hasStartTimestamp();
+    /**
+     * <code>optional int64 start_timestamp = 21;</code>
+     *
+     * <pre>
+     * execution start and end time stamp.
+     * </pre>
+     */
+    long getStartTimestamp();
+
+    // optional int64 end_timestamp = 22;
+    /**
+     * <code>optional int64 end_timestamp = 22;</code>
+     */
+    boolean hasEndTimestamp();
+    /**
+     * <code>optional int64 end_timestamp = 22;</code>
+     */
+    long getEndTimestamp();
+
+    // repeated .android.vts.CoverageReportMessage coverage = 31;
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> 
+        getCoverageList();
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.CoverageReportMessage getCoverage(int index);
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    int getCoverageCount();
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+        getCoverageOrBuilderList();
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder getCoverageOrBuilder(
+        int index);
+
+    // repeated .android.vts.ProfilingReportMessage profiling = 41;
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> 
+        getProfilingList();
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getProfiling(int index);
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    int getProfilingCount();
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+        getProfilingOrBuilderList();
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder getProfilingOrBuilder(
+        int index);
+
+    // repeated .android.vts.SystraceReportMessage systrace = 42;
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> 
+        getSystraceList();
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.SystraceReportMessage getSystrace(int index);
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    int getSystraceCount();
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+        getSystraceOrBuilderList();
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder getSystraceOrBuilder(
+        int index);
+
+    // repeated .android.vts.LogMessage log = 101;
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> 
+        getLogList();
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.LogMessage getLog(int index);
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    int getLogCount();
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+        getLogOrBuilderList();
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder getLogOrBuilder(
+        int index);
+  }
+  /**
+   * Protobuf type {@code android.vts.TestCaseReportMessage}
+   *
+   * <pre>
+   * To specify a test case execution report.
+   * </pre>
+   */
+  public static final class TestCaseReportMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements TestCaseReportMessageOrBuilder {
+    // Use TestCaseReportMessage.newBuilder() to construct.
+    private TestCaseReportMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private TestCaseReportMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final TestCaseReportMessage defaultInstance;
+    public static TestCaseReportMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public TestCaseReportMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private TestCaseReportMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              name_ = input.readBytes();
+              break;
+            }
+            case 88: {
+              int rawValue = input.readEnum();
+              com.android.vts.proto.VtsReportMessage.TestCaseResult value = com.android.vts.proto.VtsReportMessage.TestCaseResult.valueOf(rawValue);
+              if (value == null) {
+                unknownFields.mergeVarintField(11, rawValue);
+              } else {
+                bitField0_ |= 0x00000002;
+                testResult_ = value;
+              }
+              break;
+            }
+            case 168: {
+              bitField0_ |= 0x00000004;
+              startTimestamp_ = input.readInt64();
+              break;
+            }
+            case 176: {
+              bitField0_ |= 0x00000008;
+              endTimestamp_ = input.readInt64();
+              break;
+            }
+            case 250: {
+              if (!((mutable_bitField0_ & 0x00000010) == 0x00000010)) {
+                coverage_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.CoverageReportMessage>();
+                mutable_bitField0_ |= 0x00000010;
+              }
+              coverage_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.CoverageReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 330: {
+              if (!((mutable_bitField0_ & 0x00000020) == 0x00000020)) {
+                profiling_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage>();
+                mutable_bitField0_ |= 0x00000020;
+              }
+              profiling_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 338: {
+              if (!((mutable_bitField0_ & 0x00000040) == 0x00000040)) {
+                systrace_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.SystraceReportMessage>();
+                mutable_bitField0_ |= 0x00000040;
+              }
+              systrace_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.SystraceReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 810: {
+              if (!((mutable_bitField0_ & 0x00000080) == 0x00000080)) {
+                log_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.LogMessage>();
+                mutable_bitField0_ |= 0x00000080;
+              }
+              log_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.LogMessage.PARSER, extensionRegistry));
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000010) == 0x00000010)) {
+          coverage_ = java.util.Collections.unmodifiableList(coverage_);
+        }
+        if (((mutable_bitField0_ & 0x00000020) == 0x00000020)) {
+          profiling_ = java.util.Collections.unmodifiableList(profiling_);
+        }
+        if (((mutable_bitField0_ & 0x00000040) == 0x00000040)) {
+          systrace_ = java.util.Collections.unmodifiableList(systrace_);
+        }
+        if (((mutable_bitField0_ & 0x00000080) == 0x00000080)) {
+          log_ = java.util.Collections.unmodifiableList(log_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestCaseReportMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestCaseReportMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.class, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<TestCaseReportMessage> PARSER =
+        new com.google.protobuf.AbstractParser<TestCaseReportMessage>() {
+      public TestCaseReportMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new TestCaseReportMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<TestCaseReportMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes name = 1;
+    public static final int NAME_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString name_;
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the test case name.
+     * </pre>
+     */
+    public boolean hasName() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the test case name.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getName() {
+      return name_;
+    }
+
+    // optional .android.vts.TestCaseResult test_result = 11;
+    public static final int TEST_RESULT_FIELD_NUMBER = 11;
+    private com.android.vts.proto.VtsReportMessage.TestCaseResult testResult_;
+    /**
+     * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+     *
+     * <pre>
+     * the test result.
+     * </pre>
+     */
+    public boolean hasTestResult() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+     *
+     * <pre>
+     * the test result.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestCaseResult getTestResult() {
+      return testResult_;
+    }
+
+    // optional int64 start_timestamp = 21;
+    public static final int START_TIMESTAMP_FIELD_NUMBER = 21;
+    private long startTimestamp_;
+    /**
+     * <code>optional int64 start_timestamp = 21;</code>
+     *
+     * <pre>
+     * execution start and end time stamp.
+     * </pre>
+     */
+    public boolean hasStartTimestamp() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional int64 start_timestamp = 21;</code>
+     *
+     * <pre>
+     * execution start and end time stamp.
+     * </pre>
+     */
+    public long getStartTimestamp() {
+      return startTimestamp_;
+    }
+
+    // optional int64 end_timestamp = 22;
+    public static final int END_TIMESTAMP_FIELD_NUMBER = 22;
+    private long endTimestamp_;
+    /**
+     * <code>optional int64 end_timestamp = 22;</code>
+     */
+    public boolean hasEndTimestamp() {
+      return ((bitField0_ & 0x00000008) == 0x00000008);
+    }
+    /**
+     * <code>optional int64 end_timestamp = 22;</code>
+     */
+    public long getEndTimestamp() {
+      return endTimestamp_;
+    }
+
+    // repeated .android.vts.CoverageReportMessage coverage = 31;
+    public static final int COVERAGE_FIELD_NUMBER = 31;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> coverage_;
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> getCoverageList() {
+      return coverage_;
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+        getCoverageOrBuilderList() {
+      return coverage_;
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    public int getCoverageCount() {
+      return coverage_.size();
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.CoverageReportMessage getCoverage(int index) {
+      return coverage_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+     *
+     * <pre>
+     * coverage report per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder getCoverageOrBuilder(
+        int index) {
+      return coverage_.get(index);
+    }
+
+    // repeated .android.vts.ProfilingReportMessage profiling = 41;
+    public static final int PROFILING_FIELD_NUMBER = 41;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> profiling_;
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> getProfilingList() {
+      return profiling_;
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+        getProfilingOrBuilderList() {
+      return profiling_;
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    public int getProfilingCount() {
+      return profiling_.size();
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getProfiling(int index) {
+      return profiling_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+     *
+     * <pre>
+     * profiling reports
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder getProfilingOrBuilder(
+        int index) {
+      return profiling_.get(index);
+    }
+
+    // repeated .android.vts.SystraceReportMessage systrace = 42;
+    public static final int SYSTRACE_FIELD_NUMBER = 42;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> systrace_;
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> getSystraceList() {
+      return systrace_;
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+        getSystraceOrBuilderList() {
+      return systrace_;
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    public int getSystraceCount() {
+      return systrace_.size();
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.SystraceReportMessage getSystrace(int index) {
+      return systrace_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+     *
+     * <pre>
+     * systrace report message per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder getSystraceOrBuilder(
+        int index) {
+      return systrace_.get(index);
+    }
+
+    // repeated .android.vts.LogMessage log = 101;
+    public static final int LOG_FIELD_NUMBER = 101;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> log_;
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> getLogList() {
+      return log_;
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+        getLogOrBuilderList() {
+      return log_;
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public int getLogCount() {
+      return log_.size();
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.LogMessage getLog(int index) {
+      return log_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 101;</code>
+     *
+     * <pre>
+     * log for each test case. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder getLogOrBuilder(
+        int index) {
+      return log_.get(index);
+    }
+
+    private void initFields() {
+      name_ = com.google.protobuf.ByteString.EMPTY;
+      testResult_ = com.android.vts.proto.VtsReportMessage.TestCaseResult.UNKNOWN_RESULT;
+      startTimestamp_ = 0L;
+      endTimestamp_ = 0L;
+      coverage_ = java.util.Collections.emptyList();
+      profiling_ = java.util.Collections.emptyList();
+      systrace_ = java.util.Collections.emptyList();
+      log_ = java.util.Collections.emptyList();
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, name_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeEnum(11, testResult_.getNumber());
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeInt64(21, startTimestamp_);
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        output.writeInt64(22, endTimestamp_);
+      }
+      for (int i = 0; i < coverage_.size(); i++) {
+        output.writeMessage(31, coverage_.get(i));
+      }
+      for (int i = 0; i < profiling_.size(); i++) {
+        output.writeMessage(41, profiling_.get(i));
+      }
+      for (int i = 0; i < systrace_.size(); i++) {
+        output.writeMessage(42, systrace_.get(i));
+      }
+      for (int i = 0; i < log_.size(); i++) {
+        output.writeMessage(101, log_.get(i));
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, name_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeEnumSize(11, testResult_.getNumber());
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(21, startTimestamp_);
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(22, endTimestamp_);
+      }
+      for (int i = 0; i < coverage_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(31, coverage_.get(i));
+      }
+      for (int i = 0; i < profiling_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(41, profiling_.get(i));
+      }
+      for (int i = 0; i < systrace_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(42, systrace_.get(i));
+      }
+      for (int i = 0; i < log_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(101, log_.get(i));
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.TestCaseReportMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.TestCaseReportMessage}
+     *
+     * <pre>
+     * To specify a test case execution report.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestCaseReportMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestCaseReportMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.class, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+          getCoverageFieldBuilder();
+          getProfilingFieldBuilder();
+          getSystraceFieldBuilder();
+          getLogFieldBuilder();
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        name_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        testResult_ = com.android.vts.proto.VtsReportMessage.TestCaseResult.UNKNOWN_RESULT;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        startTimestamp_ = 0L;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        endTimestamp_ = 0L;
+        bitField0_ = (bitField0_ & ~0x00000008);
+        if (coverageBuilder_ == null) {
+          coverage_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000010);
+        } else {
+          coverageBuilder_.clear();
+        }
+        if (profilingBuilder_ == null) {
+          profiling_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000020);
+        } else {
+          profilingBuilder_.clear();
+        }
+        if (systraceBuilder_ == null) {
+          systrace_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000040);
+        } else {
+          systraceBuilder_.clear();
+        }
+        if (logBuilder_ == null) {
+          log_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000080);
+        } else {
+          logBuilder_.clear();
+        }
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestCaseReportMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage build() {
+        com.android.vts.proto.VtsReportMessage.TestCaseReportMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.TestCaseReportMessage result = new com.android.vts.proto.VtsReportMessage.TestCaseReportMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.name_ = name_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.testResult_ = testResult_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.startTimestamp_ = startTimestamp_;
+        if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
+          to_bitField0_ |= 0x00000008;
+        }
+        result.endTimestamp_ = endTimestamp_;
+        if (coverageBuilder_ == null) {
+          if (((bitField0_ & 0x00000010) == 0x00000010)) {
+            coverage_ = java.util.Collections.unmodifiableList(coverage_);
+            bitField0_ = (bitField0_ & ~0x00000010);
+          }
+          result.coverage_ = coverage_;
+        } else {
+          result.coverage_ = coverageBuilder_.build();
+        }
+        if (profilingBuilder_ == null) {
+          if (((bitField0_ & 0x00000020) == 0x00000020)) {
+            profiling_ = java.util.Collections.unmodifiableList(profiling_);
+            bitField0_ = (bitField0_ & ~0x00000020);
+          }
+          result.profiling_ = profiling_;
+        } else {
+          result.profiling_ = profilingBuilder_.build();
+        }
+        if (systraceBuilder_ == null) {
+          if (((bitField0_ & 0x00000040) == 0x00000040)) {
+            systrace_ = java.util.Collections.unmodifiableList(systrace_);
+            bitField0_ = (bitField0_ & ~0x00000040);
+          }
+          result.systrace_ = systrace_;
+        } else {
+          result.systrace_ = systraceBuilder_.build();
+        }
+        if (logBuilder_ == null) {
+          if (((bitField0_ & 0x00000080) == 0x00000080)) {
+            log_ = java.util.Collections.unmodifiableList(log_);
+            bitField0_ = (bitField0_ & ~0x00000080);
+          }
+          result.log_ = log_;
+        } else {
+          result.log_ = logBuilder_.build();
+        }
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.TestCaseReportMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.TestCaseReportMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.TestCaseReportMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.getDefaultInstance()) return this;
+        if (other.hasName()) {
+          setName(other.getName());
+        }
+        if (other.hasTestResult()) {
+          setTestResult(other.getTestResult());
+        }
+        if (other.hasStartTimestamp()) {
+          setStartTimestamp(other.getStartTimestamp());
+        }
+        if (other.hasEndTimestamp()) {
+          setEndTimestamp(other.getEndTimestamp());
+        }
+        if (coverageBuilder_ == null) {
+          if (!other.coverage_.isEmpty()) {
+            if (coverage_.isEmpty()) {
+              coverage_ = other.coverage_;
+              bitField0_ = (bitField0_ & ~0x00000010);
+            } else {
+              ensureCoverageIsMutable();
+              coverage_.addAll(other.coverage_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.coverage_.isEmpty()) {
+            if (coverageBuilder_.isEmpty()) {
+              coverageBuilder_.dispose();
+              coverageBuilder_ = null;
+              coverage_ = other.coverage_;
+              bitField0_ = (bitField0_ & ~0x00000010);
+              coverageBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getCoverageFieldBuilder() : null;
+            } else {
+              coverageBuilder_.addAllMessages(other.coverage_);
+            }
+          }
+        }
+        if (profilingBuilder_ == null) {
+          if (!other.profiling_.isEmpty()) {
+            if (profiling_.isEmpty()) {
+              profiling_ = other.profiling_;
+              bitField0_ = (bitField0_ & ~0x00000020);
+            } else {
+              ensureProfilingIsMutable();
+              profiling_.addAll(other.profiling_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.profiling_.isEmpty()) {
+            if (profilingBuilder_.isEmpty()) {
+              profilingBuilder_.dispose();
+              profilingBuilder_ = null;
+              profiling_ = other.profiling_;
+              bitField0_ = (bitField0_ & ~0x00000020);
+              profilingBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getProfilingFieldBuilder() : null;
+            } else {
+              profilingBuilder_.addAllMessages(other.profiling_);
+            }
+          }
+        }
+        if (systraceBuilder_ == null) {
+          if (!other.systrace_.isEmpty()) {
+            if (systrace_.isEmpty()) {
+              systrace_ = other.systrace_;
+              bitField0_ = (bitField0_ & ~0x00000040);
+            } else {
+              ensureSystraceIsMutable();
+              systrace_.addAll(other.systrace_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.systrace_.isEmpty()) {
+            if (systraceBuilder_.isEmpty()) {
+              systraceBuilder_.dispose();
+              systraceBuilder_ = null;
+              systrace_ = other.systrace_;
+              bitField0_ = (bitField0_ & ~0x00000040);
+              systraceBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getSystraceFieldBuilder() : null;
+            } else {
+              systraceBuilder_.addAllMessages(other.systrace_);
+            }
+          }
+        }
+        if (logBuilder_ == null) {
+          if (!other.log_.isEmpty()) {
+            if (log_.isEmpty()) {
+              log_ = other.log_;
+              bitField0_ = (bitField0_ & ~0x00000080);
+            } else {
+              ensureLogIsMutable();
+              log_.addAll(other.log_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.log_.isEmpty()) {
+            if (logBuilder_.isEmpty()) {
+              logBuilder_.dispose();
+              logBuilder_ = null;
+              log_ = other.log_;
+              bitField0_ = (bitField0_ & ~0x00000080);
+              logBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getLogFieldBuilder() : null;
+            } else {
+              logBuilder_.addAllMessages(other.log_);
+            }
+          }
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.TestCaseReportMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.TestCaseReportMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes name = 1;
+      private com.google.protobuf.ByteString name_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the test case name.
+       * </pre>
+       */
+      public boolean hasName() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the test case name.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getName() {
+        return name_;
+      }
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the test case name.
+       * </pre>
+       */
+      public Builder setName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        name_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the test case name.
+       * </pre>
+       */
+      public Builder clearName() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        name_ = getDefaultInstance().getName();
+        onChanged();
+        return this;
+      }
+
+      // optional .android.vts.TestCaseResult test_result = 11;
+      private com.android.vts.proto.VtsReportMessage.TestCaseResult testResult_ = com.android.vts.proto.VtsReportMessage.TestCaseResult.UNKNOWN_RESULT;
+      /**
+       * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+       *
+       * <pre>
+       * the test result.
+       * </pre>
+       */
+      public boolean hasTestResult() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+       *
+       * <pre>
+       * the test result.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestCaseResult getTestResult() {
+        return testResult_;
+      }
+      /**
+       * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+       *
+       * <pre>
+       * the test result.
+       * </pre>
+       */
+      public Builder setTestResult(com.android.vts.proto.VtsReportMessage.TestCaseResult value) {
+        if (value == null) {
+          throw new NullPointerException();
+        }
+        bitField0_ |= 0x00000002;
+        testResult_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.TestCaseResult test_result = 11;</code>
+       *
+       * <pre>
+       * the test result.
+       * </pre>
+       */
+      public Builder clearTestResult() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        testResult_ = com.android.vts.proto.VtsReportMessage.TestCaseResult.UNKNOWN_RESULT;
+        onChanged();
+        return this;
+      }
+
+      // optional int64 start_timestamp = 21;
+      private long startTimestamp_ ;
+      /**
+       * <code>optional int64 start_timestamp = 21;</code>
+       *
+       * <pre>
+       * execution start and end time stamp.
+       * </pre>
+       */
+      public boolean hasStartTimestamp() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional int64 start_timestamp = 21;</code>
+       *
+       * <pre>
+       * execution start and end time stamp.
+       * </pre>
+       */
+      public long getStartTimestamp() {
+        return startTimestamp_;
+      }
+      /**
+       * <code>optional int64 start_timestamp = 21;</code>
+       *
+       * <pre>
+       * execution start and end time stamp.
+       * </pre>
+       */
+      public Builder setStartTimestamp(long value) {
+        bitField0_ |= 0x00000004;
+        startTimestamp_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int64 start_timestamp = 21;</code>
+       *
+       * <pre>
+       * execution start and end time stamp.
+       * </pre>
+       */
+      public Builder clearStartTimestamp() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        startTimestamp_ = 0L;
+        onChanged();
+        return this;
+      }
+
+      // optional int64 end_timestamp = 22;
+      private long endTimestamp_ ;
+      /**
+       * <code>optional int64 end_timestamp = 22;</code>
+       */
+      public boolean hasEndTimestamp() {
+        return ((bitField0_ & 0x00000008) == 0x00000008);
+      }
+      /**
+       * <code>optional int64 end_timestamp = 22;</code>
+       */
+      public long getEndTimestamp() {
+        return endTimestamp_;
+      }
+      /**
+       * <code>optional int64 end_timestamp = 22;</code>
+       */
+      public Builder setEndTimestamp(long value) {
+        bitField0_ |= 0x00000008;
+        endTimestamp_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int64 end_timestamp = 22;</code>
+       */
+      public Builder clearEndTimestamp() {
+        bitField0_ = (bitField0_ & ~0x00000008);
+        endTimestamp_ = 0L;
+        onChanged();
+        return this;
+      }
+
+      // repeated .android.vts.CoverageReportMessage coverage = 31;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> coverage_ =
+        java.util.Collections.emptyList();
+      private void ensureCoverageIsMutable() {
+        if (!((bitField0_ & 0x00000010) == 0x00000010)) {
+          coverage_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.CoverageReportMessage>(coverage_);
+          bitField0_ |= 0x00000010;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.CoverageReportMessage, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder, com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> coverageBuilder_;
+
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> getCoverageList() {
+        if (coverageBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(coverage_);
+        } else {
+          return coverageBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public int getCoverageCount() {
+        if (coverageBuilder_ == null) {
+          return coverage_.size();
+        } else {
+          return coverageBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage getCoverage(int index) {
+        if (coverageBuilder_ == null) {
+          return coverage_.get(index);
+        } else {
+          return coverageBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder setCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage value) {
+        if (coverageBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureCoverageIsMutable();
+          coverage_.set(index, value);
+          onChanged();
+        } else {
+          coverageBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder setCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder builderForValue) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          coverageBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(com.android.vts.proto.VtsReportMessage.CoverageReportMessage value) {
+        if (coverageBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureCoverageIsMutable();
+          coverage_.add(value);
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage value) {
+        if (coverageBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureCoverageIsMutable();
+          coverage_.add(index, value);
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(
+          com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder builderForValue) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.add(builderForValue.build());
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder builderForValue) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder addAllCoverage(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessage> values) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          super.addAll(values, coverage_);
+          onChanged();
+        } else {
+          coverageBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder clearCoverage() {
+        if (coverageBuilder_ == null) {
+          coverage_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000010);
+          onChanged();
+        } else {
+          coverageBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public Builder removeCoverage(int index) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.remove(index);
+          onChanged();
+        } else {
+          coverageBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder getCoverageBuilder(
+          int index) {
+        return getCoverageFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder getCoverageOrBuilder(
+          int index) {
+        if (coverageBuilder_ == null) {
+          return coverage_.get(index);  } else {
+          return coverageBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+           getCoverageOrBuilderList() {
+        if (coverageBuilder_ != null) {
+          return coverageBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(coverage_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder addCoverageBuilder() {
+        return getCoverageFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.CoverageReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder addCoverageBuilder(
+          int index) {
+        return getCoverageFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 31;</code>
+       *
+       * <pre>
+       * coverage report per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder> 
+           getCoverageBuilderList() {
+        return getCoverageFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.CoverageReportMessage, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder, com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+          getCoverageFieldBuilder() {
+        if (coverageBuilder_ == null) {
+          coverageBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.CoverageReportMessage, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder, com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder>(
+                  coverage_,
+                  ((bitField0_ & 0x00000010) == 0x00000010),
+                  getParentForChildren(),
+                  isClean());
+          coverage_ = null;
+        }
+        return coverageBuilder_;
+      }
+
+      // repeated .android.vts.ProfilingReportMessage profiling = 41;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> profiling_ =
+        java.util.Collections.emptyList();
+      private void ensureProfilingIsMutable() {
+        if (!((bitField0_ & 0x00000020) == 0x00000020)) {
+          profiling_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage>(profiling_);
+          bitField0_ |= 0x00000020;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.ProfilingReportMessage, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder, com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> profilingBuilder_;
+
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> getProfilingList() {
+        if (profilingBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(profiling_);
+        } else {
+          return profilingBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public int getProfilingCount() {
+        if (profilingBuilder_ == null) {
+          return profiling_.size();
+        } else {
+          return profilingBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getProfiling(int index) {
+        if (profilingBuilder_ == null) {
+          return profiling_.get(index);
+        } else {
+          return profilingBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder setProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage value) {
+        if (profilingBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureProfilingIsMutable();
+          profiling_.set(index, value);
+          onChanged();
+        } else {
+          profilingBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder setProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder builderForValue) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          profilingBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(com.android.vts.proto.VtsReportMessage.ProfilingReportMessage value) {
+        if (profilingBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureProfilingIsMutable();
+          profiling_.add(value);
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage value) {
+        if (profilingBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureProfilingIsMutable();
+          profiling_.add(index, value);
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(
+          com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder builderForValue) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.add(builderForValue.build());
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder builderForValue) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder addAllProfiling(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> values) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          super.addAll(values, profiling_);
+          onChanged();
+        } else {
+          profilingBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder clearProfiling() {
+        if (profilingBuilder_ == null) {
+          profiling_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000020);
+          onChanged();
+        } else {
+          profilingBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public Builder removeProfiling(int index) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.remove(index);
+          onChanged();
+        } else {
+          profilingBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder getProfilingBuilder(
+          int index) {
+        return getProfilingFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder getProfilingOrBuilder(
+          int index) {
+        if (profilingBuilder_ == null) {
+          return profiling_.get(index);  } else {
+          return profilingBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+           getProfilingOrBuilderList() {
+        if (profilingBuilder_ != null) {
+          return profilingBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(profiling_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder addProfilingBuilder() {
+        return getProfilingFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder addProfilingBuilder(
+          int index) {
+        return getProfilingFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 41;</code>
+       *
+       * <pre>
+       * profiling reports
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder> 
+           getProfilingBuilderList() {
+        return getProfilingFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.ProfilingReportMessage, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder, com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+          getProfilingFieldBuilder() {
+        if (profilingBuilder_ == null) {
+          profilingBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.ProfilingReportMessage, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder, com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder>(
+                  profiling_,
+                  ((bitField0_ & 0x00000020) == 0x00000020),
+                  getParentForChildren(),
+                  isClean());
+          profiling_ = null;
+        }
+        return profilingBuilder_;
+      }
+
+      // repeated .android.vts.SystraceReportMessage systrace = 42;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> systrace_ =
+        java.util.Collections.emptyList();
+      private void ensureSystraceIsMutable() {
+        if (!((bitField0_ & 0x00000040) == 0x00000040)) {
+          systrace_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.SystraceReportMessage>(systrace_);
+          bitField0_ |= 0x00000040;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.SystraceReportMessage, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder, com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> systraceBuilder_;
+
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> getSystraceList() {
+        if (systraceBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(systrace_);
+        } else {
+          return systraceBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public int getSystraceCount() {
+        if (systraceBuilder_ == null) {
+          return systrace_.size();
+        } else {
+          return systraceBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage getSystrace(int index) {
+        if (systraceBuilder_ == null) {
+          return systrace_.get(index);
+        } else {
+          return systraceBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder setSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage value) {
+        if (systraceBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureSystraceIsMutable();
+          systrace_.set(index, value);
+          onChanged();
+        } else {
+          systraceBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder setSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder builderForValue) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          systraceBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder addSystrace(com.android.vts.proto.VtsReportMessage.SystraceReportMessage value) {
+        if (systraceBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureSystraceIsMutable();
+          systrace_.add(value);
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder addSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage value) {
+        if (systraceBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureSystraceIsMutable();
+          systrace_.add(index, value);
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder addSystrace(
+          com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder builderForValue) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.add(builderForValue.build());
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder addSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder builderForValue) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder addAllSystrace(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessage> values) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          super.addAll(values, systrace_);
+          onChanged();
+        } else {
+          systraceBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder clearSystrace() {
+        if (systraceBuilder_ == null) {
+          systrace_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000040);
+          onChanged();
+        } else {
+          systraceBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public Builder removeSystrace(int index) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.remove(index);
+          onChanged();
+        } else {
+          systraceBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder getSystraceBuilder(
+          int index) {
+        return getSystraceFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder getSystraceOrBuilder(
+          int index) {
+        if (systraceBuilder_ == null) {
+          return systrace_.get(index);  } else {
+          return systraceBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+           getSystraceOrBuilderList() {
+        if (systraceBuilder_ != null) {
+          return systraceBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(systrace_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder addSystraceBuilder() {
+        return getSystraceFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.SystraceReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder addSystraceBuilder(
+          int index) {
+        return getSystraceFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 42;</code>
+       *
+       * <pre>
+       * systrace report message per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder> 
+           getSystraceBuilderList() {
+        return getSystraceFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.SystraceReportMessage, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder, com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+          getSystraceFieldBuilder() {
+        if (systraceBuilder_ == null) {
+          systraceBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.SystraceReportMessage, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder, com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder>(
+                  systrace_,
+                  ((bitField0_ & 0x00000040) == 0x00000040),
+                  getParentForChildren(),
+                  isClean());
+          systrace_ = null;
+        }
+        return systraceBuilder_;
+      }
+
+      // repeated .android.vts.LogMessage log = 101;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> log_ =
+        java.util.Collections.emptyList();
+      private void ensureLogIsMutable() {
+        if (!((bitField0_ & 0x00000080) == 0x00000080)) {
+          log_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.LogMessage>(log_);
+          bitField0_ |= 0x00000080;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.LogMessage, com.android.vts.proto.VtsReportMessage.LogMessage.Builder, com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> logBuilder_;
+
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> getLogList() {
+        if (logBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(log_);
+        } else {
+          return logBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public int getLogCount() {
+        if (logBuilder_ == null) {
+          return log_.size();
+        } else {
+          return logBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage getLog(int index) {
+        if (logBuilder_ == null) {
+          return log_.get(index);
+        } else {
+          return logBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder setLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage value) {
+        if (logBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureLogIsMutable();
+          log_.set(index, value);
+          onChanged();
+        } else {
+          logBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder setLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage.Builder builderForValue) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          logBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(com.android.vts.proto.VtsReportMessage.LogMessage value) {
+        if (logBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureLogIsMutable();
+          log_.add(value);
+          onChanged();
+        } else {
+          logBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage value) {
+        if (logBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureLogIsMutable();
+          log_.add(index, value);
+          onChanged();
+        } else {
+          logBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(
+          com.android.vts.proto.VtsReportMessage.LogMessage.Builder builderForValue) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.add(builderForValue.build());
+          onChanged();
+        } else {
+          logBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage.Builder builderForValue) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          logBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addAllLog(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.LogMessage> values) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          super.addAll(values, log_);
+          onChanged();
+        } else {
+          logBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder clearLog() {
+        if (logBuilder_ == null) {
+          log_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000080);
+          onChanged();
+        } else {
+          logBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder removeLog(int index) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.remove(index);
+          onChanged();
+        } else {
+          logBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage.Builder getLogBuilder(
+          int index) {
+        return getLogFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder getLogOrBuilder(
+          int index) {
+        if (logBuilder_ == null) {
+          return log_.get(index);  } else {
+          return logBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+           getLogOrBuilderList() {
+        if (logBuilder_ != null) {
+          return logBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(log_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage.Builder addLogBuilder() {
+        return getLogFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.LogMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage.Builder addLogBuilder(
+          int index) {
+        return getLogFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.LogMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 101;</code>
+       *
+       * <pre>
+       * log for each test case. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage.Builder> 
+           getLogBuilderList() {
+        return getLogFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.LogMessage, com.android.vts.proto.VtsReportMessage.LogMessage.Builder, com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+          getLogFieldBuilder() {
+        if (logBuilder_ == null) {
+          logBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.LogMessage, com.android.vts.proto.VtsReportMessage.LogMessage.Builder, com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder>(
+                  log_,
+                  ((bitField0_ & 0x00000080) == 0x00000080),
+                  getParentForChildren(),
+                  isClean());
+          log_ = null;
+        }
+        return logBuilder_;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.TestCaseReportMessage)
+    }
+
+    static {
+      defaultInstance = new TestCaseReportMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.TestCaseReportMessage)
+  }
+
+  public interface ProfilingReportMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes name = 1;
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the instrumentation point name.
+     * </pre>
+     */
+    boolean hasName();
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the instrumentation point name.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getName();
+
+    // optional .android.vts.VtsProfilingType type = 2;
+    /**
+     * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+     */
+    boolean hasType();
+    /**
+     * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+     */
+    com.android.vts.proto.VtsReportMessage.VtsProfilingType getType();
+
+    // optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;
+    /**
+     * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+     */
+    boolean hasRegressionMode();
+    /**
+     * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+     */
+    com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode getRegressionMode();
+
+    // optional int64 start_timestamp = 11;
+    /**
+     * <code>optional int64 start_timestamp = 11;</code>
+     *
+     * <pre>
+     * profiling start and end time stamp (for performance).
+     * </pre>
+     */
+    boolean hasStartTimestamp();
+    /**
+     * <code>optional int64 start_timestamp = 11;</code>
+     *
+     * <pre>
+     * profiling start and end time stamp (for performance).
+     * </pre>
+     */
+    long getStartTimestamp();
+
+    // optional int64 end_timestamp = 12;
+    /**
+     * <code>optional int64 end_timestamp = 12;</code>
+     */
+    boolean hasEndTimestamp();
+    /**
+     * <code>optional int64 end_timestamp = 12;</code>
+     */
+    long getEndTimestamp();
+
+    // repeated bytes label = 21;
+    /**
+     * <code>repeated bytes label = 21;</code>
+     */
+    java.util.List<com.google.protobuf.ByteString> getLabelList();
+    /**
+     * <code>repeated bytes label = 21;</code>
+     */
+    int getLabelCount();
+    /**
+     * <code>repeated bytes label = 21;</code>
+     */
+    com.google.protobuf.ByteString getLabel(int index);
+
+    // repeated int64 value = 22;
+    /**
+     * <code>repeated int64 value = 22;</code>
+     */
+    java.util.List<java.lang.Long> getValueList();
+    /**
+     * <code>repeated int64 value = 22;</code>
+     */
+    int getValueCount();
+    /**
+     * <code>repeated int64 value = 22;</code>
+     */
+    long getValue(int index);
+
+    // optional bytes x_axis_label = 31;
+    /**
+     * <code>optional bytes x_axis_label = 31;</code>
+     *
+     * <pre>
+     * x-axis and y-axis title labels when displaying the data as a graph
+     * </pre>
+     */
+    boolean hasXAxisLabel();
+    /**
+     * <code>optional bytes x_axis_label = 31;</code>
+     *
+     * <pre>
+     * x-axis and y-axis title labels when displaying the data as a graph
+     * </pre>
+     */
+    com.google.protobuf.ByteString getXAxisLabel();
+
+    // optional bytes y_axis_label = 32;
+    /**
+     * <code>optional bytes y_axis_label = 32;</code>
+     */
+    boolean hasYAxisLabel();
+    /**
+     * <code>optional bytes y_axis_label = 32;</code>
+     */
+    com.google.protobuf.ByteString getYAxisLabel();
+
+    // repeated bytes options = 41;
+    /**
+     * <code>repeated bytes options = 41;</code>
+     *
+     * <pre>
+     * a list of strings where each string has the form of 'key=value'.
+     * used to tell certain properties of the data (e.g., passthrough vs.
+     * binderized).
+     * </pre>
+     */
+    java.util.List<com.google.protobuf.ByteString> getOptionsList();
+    /**
+     * <code>repeated bytes options = 41;</code>
+     *
+     * <pre>
+     * a list of strings where each string has the form of 'key=value'.
+     * used to tell certain properties of the data (e.g., passthrough vs.
+     * binderized).
+     * </pre>
+     */
+    int getOptionsCount();
+    /**
+     * <code>repeated bytes options = 41;</code>
+     *
+     * <pre>
+     * a list of strings where each string has the form of 'key=value'.
+     * used to tell certain properties of the data (e.g., passthrough vs.
+     * binderized).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getOptions(int index);
+  }
+  /**
+   * Protobuf type {@code android.vts.ProfilingReportMessage}
+   *
+   * <pre>
+   * To specify a profiling report.
+   * </pre>
+   */
+  public static final class ProfilingReportMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements ProfilingReportMessageOrBuilder {
+    // Use ProfilingReportMessage.newBuilder() to construct.
+    private ProfilingReportMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private ProfilingReportMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final ProfilingReportMessage defaultInstance;
+    public static ProfilingReportMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public ProfilingReportMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private ProfilingReportMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              name_ = input.readBytes();
+              break;
+            }
+            case 16: {
+              int rawValue = input.readEnum();
+              com.android.vts.proto.VtsReportMessage.VtsProfilingType value = com.android.vts.proto.VtsReportMessage.VtsProfilingType.valueOf(rawValue);
+              if (value == null) {
+                unknownFields.mergeVarintField(2, rawValue);
+              } else {
+                bitField0_ |= 0x00000002;
+                type_ = value;
+              }
+              break;
+            }
+            case 24: {
+              int rawValue = input.readEnum();
+              com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode value = com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode.valueOf(rawValue);
+              if (value == null) {
+                unknownFields.mergeVarintField(3, rawValue);
+              } else {
+                bitField0_ |= 0x00000004;
+                regressionMode_ = value;
+              }
+              break;
+            }
+            case 88: {
+              bitField0_ |= 0x00000008;
+              startTimestamp_ = input.readInt64();
+              break;
+            }
+            case 96: {
+              bitField0_ |= 0x00000010;
+              endTimestamp_ = input.readInt64();
+              break;
+            }
+            case 170: {
+              if (!((mutable_bitField0_ & 0x00000020) == 0x00000020)) {
+                label_ = new java.util.ArrayList<com.google.protobuf.ByteString>();
+                mutable_bitField0_ |= 0x00000020;
+              }
+              label_.add(input.readBytes());
+              break;
+            }
+            case 176: {
+              if (!((mutable_bitField0_ & 0x00000040) == 0x00000040)) {
+                value_ = new java.util.ArrayList<java.lang.Long>();
+                mutable_bitField0_ |= 0x00000040;
+              }
+              value_.add(input.readInt64());
+              break;
+            }
+            case 178: {
+              int length = input.readRawVarint32();
+              int limit = input.pushLimit(length);
+              if (!((mutable_bitField0_ & 0x00000040) == 0x00000040) && input.getBytesUntilLimit() > 0) {
+                value_ = new java.util.ArrayList<java.lang.Long>();
+                mutable_bitField0_ |= 0x00000040;
+              }
+              while (input.getBytesUntilLimit() > 0) {
+                value_.add(input.readInt64());
+              }
+              input.popLimit(limit);
+              break;
+            }
+            case 250: {
+              bitField0_ |= 0x00000020;
+              xAxisLabel_ = input.readBytes();
+              break;
+            }
+            case 258: {
+              bitField0_ |= 0x00000040;
+              yAxisLabel_ = input.readBytes();
+              break;
+            }
+            case 330: {
+              if (!((mutable_bitField0_ & 0x00000200) == 0x00000200)) {
+                options_ = new java.util.ArrayList<com.google.protobuf.ByteString>();
+                mutable_bitField0_ |= 0x00000200;
+              }
+              options_.add(input.readBytes());
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000020) == 0x00000020)) {
+          label_ = java.util.Collections.unmodifiableList(label_);
+        }
+        if (((mutable_bitField0_ & 0x00000040) == 0x00000040)) {
+          value_ = java.util.Collections.unmodifiableList(value_);
+        }
+        if (((mutable_bitField0_ & 0x00000200) == 0x00000200)) {
+          options_ = java.util.Collections.unmodifiableList(options_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_ProfilingReportMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_ProfilingReportMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.class, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<ProfilingReportMessage> PARSER =
+        new com.google.protobuf.AbstractParser<ProfilingReportMessage>() {
+      public ProfilingReportMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new ProfilingReportMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<ProfilingReportMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes name = 1;
+    public static final int NAME_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString name_;
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the instrumentation point name.
+     * </pre>
+     */
+    public boolean hasName() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes name = 1;</code>
+     *
+     * <pre>
+     * the instrumentation point name.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getName() {
+      return name_;
+    }
+
+    // optional .android.vts.VtsProfilingType type = 2;
+    public static final int TYPE_FIELD_NUMBER = 2;
+    private com.android.vts.proto.VtsReportMessage.VtsProfilingType type_;
+    /**
+     * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+     */
+    public boolean hasType() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+     */
+    public com.android.vts.proto.VtsReportMessage.VtsProfilingType getType() {
+      return type_;
+    }
+
+    // optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;
+    public static final int REGRESSION_MODE_FIELD_NUMBER = 3;
+    private com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode regressionMode_;
+    /**
+     * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+     */
+    public boolean hasRegressionMode() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+     */
+    public com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode getRegressionMode() {
+      return regressionMode_;
+    }
+
+    // optional int64 start_timestamp = 11;
+    public static final int START_TIMESTAMP_FIELD_NUMBER = 11;
+    private long startTimestamp_;
+    /**
+     * <code>optional int64 start_timestamp = 11;</code>
+     *
+     * <pre>
+     * profiling start and end time stamp (for performance).
+     * </pre>
+     */
+    public boolean hasStartTimestamp() {
+      return ((bitField0_ & 0x00000008) == 0x00000008);
+    }
+    /**
+     * <code>optional int64 start_timestamp = 11;</code>
+     *
+     * <pre>
+     * profiling start and end time stamp (for performance).
+     * </pre>
+     */
+    public long getStartTimestamp() {
+      return startTimestamp_;
+    }
+
+    // optional int64 end_timestamp = 12;
+    public static final int END_TIMESTAMP_FIELD_NUMBER = 12;
+    private long endTimestamp_;
+    /**
+     * <code>optional int64 end_timestamp = 12;</code>
+     */
+    public boolean hasEndTimestamp() {
+      return ((bitField0_ & 0x00000010) == 0x00000010);
+    }
+    /**
+     * <code>optional int64 end_timestamp = 12;</code>
+     */
+    public long getEndTimestamp() {
+      return endTimestamp_;
+    }
+
+    // repeated bytes label = 21;
+    public static final int LABEL_FIELD_NUMBER = 21;
+    private java.util.List<com.google.protobuf.ByteString> label_;
+    /**
+     * <code>repeated bytes label = 21;</code>
+     */
+    public java.util.List<com.google.protobuf.ByteString>
+        getLabelList() {
+      return label_;
+    }
+    /**
+     * <code>repeated bytes label = 21;</code>
+     */
+    public int getLabelCount() {
+      return label_.size();
+    }
+    /**
+     * <code>repeated bytes label = 21;</code>
+     */
+    public com.google.protobuf.ByteString getLabel(int index) {
+      return label_.get(index);
+    }
+
+    // repeated int64 value = 22;
+    public static final int VALUE_FIELD_NUMBER = 22;
+    private java.util.List<java.lang.Long> value_;
+    /**
+     * <code>repeated int64 value = 22;</code>
+     */
+    public java.util.List<java.lang.Long>
+        getValueList() {
+      return value_;
+    }
+    /**
+     * <code>repeated int64 value = 22;</code>
+     */
+    public int getValueCount() {
+      return value_.size();
+    }
+    /**
+     * <code>repeated int64 value = 22;</code>
+     */
+    public long getValue(int index) {
+      return value_.get(index);
+    }
+
+    // optional bytes x_axis_label = 31;
+    public static final int X_AXIS_LABEL_FIELD_NUMBER = 31;
+    private com.google.protobuf.ByteString xAxisLabel_;
+    /**
+     * <code>optional bytes x_axis_label = 31;</code>
+     *
+     * <pre>
+     * x-axis and y-axis title labels when displaying the data as a graph
+     * </pre>
+     */
+    public boolean hasXAxisLabel() {
+      return ((bitField0_ & 0x00000020) == 0x00000020);
+    }
+    /**
+     * <code>optional bytes x_axis_label = 31;</code>
+     *
+     * <pre>
+     * x-axis and y-axis title labels when displaying the data as a graph
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getXAxisLabel() {
+      return xAxisLabel_;
+    }
+
+    // optional bytes y_axis_label = 32;
+    public static final int Y_AXIS_LABEL_FIELD_NUMBER = 32;
+    private com.google.protobuf.ByteString yAxisLabel_;
+    /**
+     * <code>optional bytes y_axis_label = 32;</code>
+     */
+    public boolean hasYAxisLabel() {
+      return ((bitField0_ & 0x00000040) == 0x00000040);
+    }
+    /**
+     * <code>optional bytes y_axis_label = 32;</code>
+     */
+    public com.google.protobuf.ByteString getYAxisLabel() {
+      return yAxisLabel_;
+    }
+
+    // repeated bytes options = 41;
+    public static final int OPTIONS_FIELD_NUMBER = 41;
+    private java.util.List<com.google.protobuf.ByteString> options_;
+    /**
+     * <code>repeated bytes options = 41;</code>
+     *
+     * <pre>
+     * a list of strings where each string has the form of 'key=value'.
+     * used to tell certain properties of the data (e.g., passthrough vs.
+     * binderized).
+     * </pre>
+     */
+    public java.util.List<com.google.protobuf.ByteString>
+        getOptionsList() {
+      return options_;
+    }
+    /**
+     * <code>repeated bytes options = 41;</code>
+     *
+     * <pre>
+     * a list of strings where each string has the form of 'key=value'.
+     * used to tell certain properties of the data (e.g., passthrough vs.
+     * binderized).
+     * </pre>
+     */
+    public int getOptionsCount() {
+      return options_.size();
+    }
+    /**
+     * <code>repeated bytes options = 41;</code>
+     *
+     * <pre>
+     * a list of strings where each string has the form of 'key=value'.
+     * used to tell certain properties of the data (e.g., passthrough vs.
+     * binderized).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getOptions(int index) {
+      return options_.get(index);
+    }
+
+    private void initFields() {
+      name_ = com.google.protobuf.ByteString.EMPTY;
+      type_ = com.android.vts.proto.VtsReportMessage.VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE;
+      regressionMode_ = com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE;
+      startTimestamp_ = 0L;
+      endTimestamp_ = 0L;
+      label_ = java.util.Collections.emptyList();
+      value_ = java.util.Collections.emptyList();
+      xAxisLabel_ = com.google.protobuf.ByteString.EMPTY;
+      yAxisLabel_ = com.google.protobuf.ByteString.EMPTY;
+      options_ = java.util.Collections.emptyList();
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, name_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeEnum(2, type_.getNumber());
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeEnum(3, regressionMode_.getNumber());
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        output.writeInt64(11, startTimestamp_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        output.writeInt64(12, endTimestamp_);
+      }
+      for (int i = 0; i < label_.size(); i++) {
+        output.writeBytes(21, label_.get(i));
+      }
+      for (int i = 0; i < value_.size(); i++) {
+        output.writeInt64(22, value_.get(i));
+      }
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        output.writeBytes(31, xAxisLabel_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        output.writeBytes(32, yAxisLabel_);
+      }
+      for (int i = 0; i < options_.size(); i++) {
+        output.writeBytes(41, options_.get(i));
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, name_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeEnumSize(2, type_.getNumber());
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeEnumSize(3, regressionMode_.getNumber());
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(11, startTimestamp_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(12, endTimestamp_);
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < label_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeBytesSizeNoTag(label_.get(i));
+        }
+        size += dataSize;
+        size += 2 * getLabelList().size();
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < value_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeInt64SizeNoTag(value_.get(i));
+        }
+        size += dataSize;
+        size += 2 * getValueList().size();
+      }
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(31, xAxisLabel_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(32, yAxisLabel_);
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < options_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeBytesSizeNoTag(options_.get(i));
+        }
+        size += dataSize;
+        size += 2 * getOptionsList().size();
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.ProfilingReportMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.ProfilingReportMessage}
+     *
+     * <pre>
+     * To specify a profiling report.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_ProfilingReportMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_ProfilingReportMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.class, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        name_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        type_ = com.android.vts.proto.VtsReportMessage.VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        regressionMode_ = com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        startTimestamp_ = 0L;
+        bitField0_ = (bitField0_ & ~0x00000008);
+        endTimestamp_ = 0L;
+        bitField0_ = (bitField0_ & ~0x00000010);
+        label_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000020);
+        value_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000040);
+        xAxisLabel_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000080);
+        yAxisLabel_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000100);
+        options_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000200);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_ProfilingReportMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage build() {
+        com.android.vts.proto.VtsReportMessage.ProfilingReportMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.ProfilingReportMessage result = new com.android.vts.proto.VtsReportMessage.ProfilingReportMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.name_ = name_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.type_ = type_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.regressionMode_ = regressionMode_;
+        if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
+          to_bitField0_ |= 0x00000008;
+        }
+        result.startTimestamp_ = startTimestamp_;
+        if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
+          to_bitField0_ |= 0x00000010;
+        }
+        result.endTimestamp_ = endTimestamp_;
+        if (((bitField0_ & 0x00000020) == 0x00000020)) {
+          label_ = java.util.Collections.unmodifiableList(label_);
+          bitField0_ = (bitField0_ & ~0x00000020);
+        }
+        result.label_ = label_;
+        if (((bitField0_ & 0x00000040) == 0x00000040)) {
+          value_ = java.util.Collections.unmodifiableList(value_);
+          bitField0_ = (bitField0_ & ~0x00000040);
+        }
+        result.value_ = value_;
+        if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
+          to_bitField0_ |= 0x00000020;
+        }
+        result.xAxisLabel_ = xAxisLabel_;
+        if (((from_bitField0_ & 0x00000100) == 0x00000100)) {
+          to_bitField0_ |= 0x00000040;
+        }
+        result.yAxisLabel_ = yAxisLabel_;
+        if (((bitField0_ & 0x00000200) == 0x00000200)) {
+          options_ = java.util.Collections.unmodifiableList(options_);
+          bitField0_ = (bitField0_ & ~0x00000200);
+        }
+        result.options_ = options_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.ProfilingReportMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.ProfilingReportMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.ProfilingReportMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.getDefaultInstance()) return this;
+        if (other.hasName()) {
+          setName(other.getName());
+        }
+        if (other.hasType()) {
+          setType(other.getType());
+        }
+        if (other.hasRegressionMode()) {
+          setRegressionMode(other.getRegressionMode());
+        }
+        if (other.hasStartTimestamp()) {
+          setStartTimestamp(other.getStartTimestamp());
+        }
+        if (other.hasEndTimestamp()) {
+          setEndTimestamp(other.getEndTimestamp());
+        }
+        if (!other.label_.isEmpty()) {
+          if (label_.isEmpty()) {
+            label_ = other.label_;
+            bitField0_ = (bitField0_ & ~0x00000020);
+          } else {
+            ensureLabelIsMutable();
+            label_.addAll(other.label_);
+          }
+          onChanged();
+        }
+        if (!other.value_.isEmpty()) {
+          if (value_.isEmpty()) {
+            value_ = other.value_;
+            bitField0_ = (bitField0_ & ~0x00000040);
+          } else {
+            ensureValueIsMutable();
+            value_.addAll(other.value_);
+          }
+          onChanged();
+        }
+        if (other.hasXAxisLabel()) {
+          setXAxisLabel(other.getXAxisLabel());
+        }
+        if (other.hasYAxisLabel()) {
+          setYAxisLabel(other.getYAxisLabel());
+        }
+        if (!other.options_.isEmpty()) {
+          if (options_.isEmpty()) {
+            options_ = other.options_;
+            bitField0_ = (bitField0_ & ~0x00000200);
+          } else {
+            ensureOptionsIsMutable();
+            options_.addAll(other.options_);
+          }
+          onChanged();
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.ProfilingReportMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.ProfilingReportMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes name = 1;
+      private com.google.protobuf.ByteString name_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the instrumentation point name.
+       * </pre>
+       */
+      public boolean hasName() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the instrumentation point name.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getName() {
+        return name_;
+      }
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the instrumentation point name.
+       * </pre>
+       */
+      public Builder setName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        name_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes name = 1;</code>
+       *
+       * <pre>
+       * the instrumentation point name.
+       * </pre>
+       */
+      public Builder clearName() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        name_ = getDefaultInstance().getName();
+        onChanged();
+        return this;
+      }
+
+      // optional .android.vts.VtsProfilingType type = 2;
+      private com.android.vts.proto.VtsReportMessage.VtsProfilingType type_ = com.android.vts.proto.VtsReportMessage.VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE;
+      /**
+       * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+       */
+      public boolean hasType() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.VtsProfilingType getType() {
+        return type_;
+      }
+      /**
+       * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+       */
+      public Builder setType(com.android.vts.proto.VtsReportMessage.VtsProfilingType value) {
+        if (value == null) {
+          throw new NullPointerException();
+        }
+        bitField0_ |= 0x00000002;
+        type_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsProfilingType type = 2;</code>
+       */
+      public Builder clearType() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        type_ = com.android.vts.proto.VtsReportMessage.VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE;
+        onChanged();
+        return this;
+      }
+
+      // optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;
+      private com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode regressionMode_ = com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE;
+      /**
+       * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+       */
+      public boolean hasRegressionMode() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode getRegressionMode() {
+        return regressionMode_;
+      }
+      /**
+       * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+       */
+      public Builder setRegressionMode(com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode value) {
+        if (value == null) {
+          throw new NullPointerException();
+        }
+        bitField0_ |= 0x00000004;
+        regressionMode_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsProfilingRegressionMode regression_mode = 3;</code>
+       */
+      public Builder clearRegressionMode() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        regressionMode_ = com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode.UNKNOWN_REGRESSION_MODE;
+        onChanged();
+        return this;
+      }
+
+      // optional int64 start_timestamp = 11;
+      private long startTimestamp_ ;
+      /**
+       * <code>optional int64 start_timestamp = 11;</code>
+       *
+       * <pre>
+       * profiling start and end time stamp (for performance).
+       * </pre>
+       */
+      public boolean hasStartTimestamp() {
+        return ((bitField0_ & 0x00000008) == 0x00000008);
+      }
+      /**
+       * <code>optional int64 start_timestamp = 11;</code>
+       *
+       * <pre>
+       * profiling start and end time stamp (for performance).
+       * </pre>
+       */
+      public long getStartTimestamp() {
+        return startTimestamp_;
+      }
+      /**
+       * <code>optional int64 start_timestamp = 11;</code>
+       *
+       * <pre>
+       * profiling start and end time stamp (for performance).
+       * </pre>
+       */
+      public Builder setStartTimestamp(long value) {
+        bitField0_ |= 0x00000008;
+        startTimestamp_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int64 start_timestamp = 11;</code>
+       *
+       * <pre>
+       * profiling start and end time stamp (for performance).
+       * </pre>
+       */
+      public Builder clearStartTimestamp() {
+        bitField0_ = (bitField0_ & ~0x00000008);
+        startTimestamp_ = 0L;
+        onChanged();
+        return this;
+      }
+
+      // optional int64 end_timestamp = 12;
+      private long endTimestamp_ ;
+      /**
+       * <code>optional int64 end_timestamp = 12;</code>
+       */
+      public boolean hasEndTimestamp() {
+        return ((bitField0_ & 0x00000010) == 0x00000010);
+      }
+      /**
+       * <code>optional int64 end_timestamp = 12;</code>
+       */
+      public long getEndTimestamp() {
+        return endTimestamp_;
+      }
+      /**
+       * <code>optional int64 end_timestamp = 12;</code>
+       */
+      public Builder setEndTimestamp(long value) {
+        bitField0_ |= 0x00000010;
+        endTimestamp_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int64 end_timestamp = 12;</code>
+       */
+      public Builder clearEndTimestamp() {
+        bitField0_ = (bitField0_ & ~0x00000010);
+        endTimestamp_ = 0L;
+        onChanged();
+        return this;
+      }
+
+      // repeated bytes label = 21;
+      private java.util.List<com.google.protobuf.ByteString> label_ = java.util.Collections.emptyList();
+      private void ensureLabelIsMutable() {
+        if (!((bitField0_ & 0x00000020) == 0x00000020)) {
+          label_ = new java.util.ArrayList<com.google.protobuf.ByteString>(label_);
+          bitField0_ |= 0x00000020;
+         }
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public java.util.List<com.google.protobuf.ByteString>
+          getLabelList() {
+        return java.util.Collections.unmodifiableList(label_);
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public int getLabelCount() {
+        return label_.size();
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public com.google.protobuf.ByteString getLabel(int index) {
+        return label_.get(index);
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public Builder setLabel(
+          int index, com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureLabelIsMutable();
+        label_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public Builder addLabel(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureLabelIsMutable();
+        label_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public Builder addAllLabel(
+          java.lang.Iterable<? extends com.google.protobuf.ByteString> values) {
+        ensureLabelIsMutable();
+        super.addAll(values, label_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes label = 21;</code>
+       */
+      public Builder clearLabel() {
+        label_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000020);
+        onChanged();
+        return this;
+      }
+
+      // repeated int64 value = 22;
+      private java.util.List<java.lang.Long> value_ = java.util.Collections.emptyList();
+      private void ensureValueIsMutable() {
+        if (!((bitField0_ & 0x00000040) == 0x00000040)) {
+          value_ = new java.util.ArrayList<java.lang.Long>(value_);
+          bitField0_ |= 0x00000040;
+         }
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public java.util.List<java.lang.Long>
+          getValueList() {
+        return java.util.Collections.unmodifiableList(value_);
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public int getValueCount() {
+        return value_.size();
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public long getValue(int index) {
+        return value_.get(index);
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public Builder setValue(
+          int index, long value) {
+        ensureValueIsMutable();
+        value_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public Builder addValue(long value) {
+        ensureValueIsMutable();
+        value_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public Builder addAllValue(
+          java.lang.Iterable<? extends java.lang.Long> values) {
+        ensureValueIsMutable();
+        super.addAll(values, value_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int64 value = 22;</code>
+       */
+      public Builder clearValue() {
+        value_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000040);
+        onChanged();
+        return this;
+      }
+
+      // optional bytes x_axis_label = 31;
+      private com.google.protobuf.ByteString xAxisLabel_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes x_axis_label = 31;</code>
+       *
+       * <pre>
+       * x-axis and y-axis title labels when displaying the data as a graph
+       * </pre>
+       */
+      public boolean hasXAxisLabel() {
+        return ((bitField0_ & 0x00000080) == 0x00000080);
+      }
+      /**
+       * <code>optional bytes x_axis_label = 31;</code>
+       *
+       * <pre>
+       * x-axis and y-axis title labels when displaying the data as a graph
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getXAxisLabel() {
+        return xAxisLabel_;
+      }
+      /**
+       * <code>optional bytes x_axis_label = 31;</code>
+       *
+       * <pre>
+       * x-axis and y-axis title labels when displaying the data as a graph
+       * </pre>
+       */
+      public Builder setXAxisLabel(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000080;
+        xAxisLabel_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes x_axis_label = 31;</code>
+       *
+       * <pre>
+       * x-axis and y-axis title labels when displaying the data as a graph
+       * </pre>
+       */
+      public Builder clearXAxisLabel() {
+        bitField0_ = (bitField0_ & ~0x00000080);
+        xAxisLabel_ = getDefaultInstance().getXAxisLabel();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes y_axis_label = 32;
+      private com.google.protobuf.ByteString yAxisLabel_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes y_axis_label = 32;</code>
+       */
+      public boolean hasYAxisLabel() {
+        return ((bitField0_ & 0x00000100) == 0x00000100);
+      }
+      /**
+       * <code>optional bytes y_axis_label = 32;</code>
+       */
+      public com.google.protobuf.ByteString getYAxisLabel() {
+        return yAxisLabel_;
+      }
+      /**
+       * <code>optional bytes y_axis_label = 32;</code>
+       */
+      public Builder setYAxisLabel(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000100;
+        yAxisLabel_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes y_axis_label = 32;</code>
+       */
+      public Builder clearYAxisLabel() {
+        bitField0_ = (bitField0_ & ~0x00000100);
+        yAxisLabel_ = getDefaultInstance().getYAxisLabel();
+        onChanged();
+        return this;
+      }
+
+      // repeated bytes options = 41;
+      private java.util.List<com.google.protobuf.ByteString> options_ = java.util.Collections.emptyList();
+      private void ensureOptionsIsMutable() {
+        if (!((bitField0_ & 0x00000200) == 0x00000200)) {
+          options_ = new java.util.ArrayList<com.google.protobuf.ByteString>(options_);
+          bitField0_ |= 0x00000200;
+         }
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public java.util.List<com.google.protobuf.ByteString>
+          getOptionsList() {
+        return java.util.Collections.unmodifiableList(options_);
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public int getOptionsCount() {
+        return options_.size();
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getOptions(int index) {
+        return options_.get(index);
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public Builder setOptions(
+          int index, com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureOptionsIsMutable();
+        options_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public Builder addOptions(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureOptionsIsMutable();
+        options_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public Builder addAllOptions(
+          java.lang.Iterable<? extends com.google.protobuf.ByteString> values) {
+        ensureOptionsIsMutable();
+        super.addAll(values, options_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes options = 41;</code>
+       *
+       * <pre>
+       * a list of strings where each string has the form of 'key=value'.
+       * used to tell certain properties of the data (e.g., passthrough vs.
+       * binderized).
+       * </pre>
+       */
+      public Builder clearOptions() {
+        options_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000200);
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.ProfilingReportMessage)
+    }
+
+    static {
+      defaultInstance = new ProfilingReportMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.ProfilingReportMessage)
+  }
+
+  public interface SystraceReportMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes process_name = 1;
+    /**
+     * <code>optional bytes process_name = 1;</code>
+     *
+     * <pre>
+     * the target process name used by systrace
+     * </pre>
+     */
+    boolean hasProcessName();
+    /**
+     * <code>optional bytes process_name = 1;</code>
+     *
+     * <pre>
+     * the target process name used by systrace
+     * </pre>
+     */
+    com.google.protobuf.ByteString getProcessName();
+
+    // repeated bytes html = 11;
+    /**
+     * <code>repeated bytes html = 11;</code>
+     *
+     * <pre>
+     * the produced html report
+     * </pre>
+     */
+    java.util.List<com.google.protobuf.ByteString> getHtmlList();
+    /**
+     * <code>repeated bytes html = 11;</code>
+     *
+     * <pre>
+     * the produced html report
+     * </pre>
+     */
+    int getHtmlCount();
+    /**
+     * <code>repeated bytes html = 11;</code>
+     *
+     * <pre>
+     * the produced html report
+     * </pre>
+     */
+    com.google.protobuf.ByteString getHtml(int index);
+
+    // repeated bytes url = 21;
+    /**
+     * <code>repeated bytes url = 21;</code>
+     *
+     * <pre>
+     * URLs of the produced html reports
+     * </pre>
+     */
+    java.util.List<com.google.protobuf.ByteString> getUrlList();
+    /**
+     * <code>repeated bytes url = 21;</code>
+     *
+     * <pre>
+     * URLs of the produced html reports
+     * </pre>
+     */
+    int getUrlCount();
+    /**
+     * <code>repeated bytes url = 21;</code>
+     *
+     * <pre>
+     * URLs of the produced html reports
+     * </pre>
+     */
+    com.google.protobuf.ByteString getUrl(int index);
+  }
+  /**
+   * Protobuf type {@code android.vts.SystraceReportMessage}
+   *
+   * <pre>
+   * To specify a systrace report.
+   * </pre>
+   */
+  public static final class SystraceReportMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements SystraceReportMessageOrBuilder {
+    // Use SystraceReportMessage.newBuilder() to construct.
+    private SystraceReportMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private SystraceReportMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final SystraceReportMessage defaultInstance;
+    public static SystraceReportMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public SystraceReportMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private SystraceReportMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              processName_ = input.readBytes();
+              break;
+            }
+            case 90: {
+              if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
+                html_ = new java.util.ArrayList<com.google.protobuf.ByteString>();
+                mutable_bitField0_ |= 0x00000002;
+              }
+              html_.add(input.readBytes());
+              break;
+            }
+            case 170: {
+              if (!((mutable_bitField0_ & 0x00000004) == 0x00000004)) {
+                url_ = new java.util.ArrayList<com.google.protobuf.ByteString>();
+                mutable_bitField0_ |= 0x00000004;
+              }
+              url_.add(input.readBytes());
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
+          html_ = java.util.Collections.unmodifiableList(html_);
+        }
+        if (((mutable_bitField0_ & 0x00000004) == 0x00000004)) {
+          url_ = java.util.Collections.unmodifiableList(url_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_SystraceReportMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_SystraceReportMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.SystraceReportMessage.class, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<SystraceReportMessage> PARSER =
+        new com.google.protobuf.AbstractParser<SystraceReportMessage>() {
+      public SystraceReportMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new SystraceReportMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<SystraceReportMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes process_name = 1;
+    public static final int PROCESS_NAME_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString processName_;
+    /**
+     * <code>optional bytes process_name = 1;</code>
+     *
+     * <pre>
+     * the target process name used by systrace
+     * </pre>
+     */
+    public boolean hasProcessName() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes process_name = 1;</code>
+     *
+     * <pre>
+     * the target process name used by systrace
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getProcessName() {
+      return processName_;
+    }
+
+    // repeated bytes html = 11;
+    public static final int HTML_FIELD_NUMBER = 11;
+    private java.util.List<com.google.protobuf.ByteString> html_;
+    /**
+     * <code>repeated bytes html = 11;</code>
+     *
+     * <pre>
+     * the produced html report
+     * </pre>
+     */
+    public java.util.List<com.google.protobuf.ByteString>
+        getHtmlList() {
+      return html_;
+    }
+    /**
+     * <code>repeated bytes html = 11;</code>
+     *
+     * <pre>
+     * the produced html report
+     * </pre>
+     */
+    public int getHtmlCount() {
+      return html_.size();
+    }
+    /**
+     * <code>repeated bytes html = 11;</code>
+     *
+     * <pre>
+     * the produced html report
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getHtml(int index) {
+      return html_.get(index);
+    }
+
+    // repeated bytes url = 21;
+    public static final int URL_FIELD_NUMBER = 21;
+    private java.util.List<com.google.protobuf.ByteString> url_;
+    /**
+     * <code>repeated bytes url = 21;</code>
+     *
+     * <pre>
+     * URLs of the produced html reports
+     * </pre>
+     */
+    public java.util.List<com.google.protobuf.ByteString>
+        getUrlList() {
+      return url_;
+    }
+    /**
+     * <code>repeated bytes url = 21;</code>
+     *
+     * <pre>
+     * URLs of the produced html reports
+     * </pre>
+     */
+    public int getUrlCount() {
+      return url_.size();
+    }
+    /**
+     * <code>repeated bytes url = 21;</code>
+     *
+     * <pre>
+     * URLs of the produced html reports
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getUrl(int index) {
+      return url_.get(index);
+    }
+
+    private void initFields() {
+      processName_ = com.google.protobuf.ByteString.EMPTY;
+      html_ = java.util.Collections.emptyList();
+      url_ = java.util.Collections.emptyList();
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, processName_);
+      }
+      for (int i = 0; i < html_.size(); i++) {
+        output.writeBytes(11, html_.get(i));
+      }
+      for (int i = 0; i < url_.size(); i++) {
+        output.writeBytes(21, url_.get(i));
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, processName_);
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < html_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeBytesSizeNoTag(html_.get(i));
+        }
+        size += dataSize;
+        size += 1 * getHtmlList().size();
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < url_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeBytesSizeNoTag(url_.get(i));
+        }
+        size += dataSize;
+        size += 2 * getUrlList().size();
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.SystraceReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.SystraceReportMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.SystraceReportMessage}
+     *
+     * <pre>
+     * To specify a systrace report.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_SystraceReportMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_SystraceReportMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.SystraceReportMessage.class, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.SystraceReportMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        processName_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        html_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000002);
+        url_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000004);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_SystraceReportMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.SystraceReportMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage build() {
+        com.android.vts.proto.VtsReportMessage.SystraceReportMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.SystraceReportMessage result = new com.android.vts.proto.VtsReportMessage.SystraceReportMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.processName_ = processName_;
+        if (((bitField0_ & 0x00000002) == 0x00000002)) {
+          html_ = java.util.Collections.unmodifiableList(html_);
+          bitField0_ = (bitField0_ & ~0x00000002);
+        }
+        result.html_ = html_;
+        if (((bitField0_ & 0x00000004) == 0x00000004)) {
+          url_ = java.util.Collections.unmodifiableList(url_);
+          bitField0_ = (bitField0_ & ~0x00000004);
+        }
+        result.url_ = url_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.SystraceReportMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.SystraceReportMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.SystraceReportMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.SystraceReportMessage.getDefaultInstance()) return this;
+        if (other.hasProcessName()) {
+          setProcessName(other.getProcessName());
+        }
+        if (!other.html_.isEmpty()) {
+          if (html_.isEmpty()) {
+            html_ = other.html_;
+            bitField0_ = (bitField0_ & ~0x00000002);
+          } else {
+            ensureHtmlIsMutable();
+            html_.addAll(other.html_);
+          }
+          onChanged();
+        }
+        if (!other.url_.isEmpty()) {
+          if (url_.isEmpty()) {
+            url_ = other.url_;
+            bitField0_ = (bitField0_ & ~0x00000004);
+          } else {
+            ensureUrlIsMutable();
+            url_.addAll(other.url_);
+          }
+          onChanged();
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.SystraceReportMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.SystraceReportMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes process_name = 1;
+      private com.google.protobuf.ByteString processName_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes process_name = 1;</code>
+       *
+       * <pre>
+       * the target process name used by systrace
+       * </pre>
+       */
+      public boolean hasProcessName() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes process_name = 1;</code>
+       *
+       * <pre>
+       * the target process name used by systrace
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getProcessName() {
+        return processName_;
+      }
+      /**
+       * <code>optional bytes process_name = 1;</code>
+       *
+       * <pre>
+       * the target process name used by systrace
+       * </pre>
+       */
+      public Builder setProcessName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        processName_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes process_name = 1;</code>
+       *
+       * <pre>
+       * the target process name used by systrace
+       * </pre>
+       */
+      public Builder clearProcessName() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        processName_ = getDefaultInstance().getProcessName();
+        onChanged();
+        return this;
+      }
+
+      // repeated bytes html = 11;
+      private java.util.List<com.google.protobuf.ByteString> html_ = java.util.Collections.emptyList();
+      private void ensureHtmlIsMutable() {
+        if (!((bitField0_ & 0x00000002) == 0x00000002)) {
+          html_ = new java.util.ArrayList<com.google.protobuf.ByteString>(html_);
+          bitField0_ |= 0x00000002;
+         }
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public java.util.List<com.google.protobuf.ByteString>
+          getHtmlList() {
+        return java.util.Collections.unmodifiableList(html_);
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public int getHtmlCount() {
+        return html_.size();
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getHtml(int index) {
+        return html_.get(index);
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public Builder setHtml(
+          int index, com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureHtmlIsMutable();
+        html_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public Builder addHtml(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureHtmlIsMutable();
+        html_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public Builder addAllHtml(
+          java.lang.Iterable<? extends com.google.protobuf.ByteString> values) {
+        ensureHtmlIsMutable();
+        super.addAll(values, html_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes html = 11;</code>
+       *
+       * <pre>
+       * the produced html report
+       * </pre>
+       */
+      public Builder clearHtml() {
+        html_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000002);
+        onChanged();
+        return this;
+      }
+
+      // repeated bytes url = 21;
+      private java.util.List<com.google.protobuf.ByteString> url_ = java.util.Collections.emptyList();
+      private void ensureUrlIsMutable() {
+        if (!((bitField0_ & 0x00000004) == 0x00000004)) {
+          url_ = new java.util.ArrayList<com.google.protobuf.ByteString>(url_);
+          bitField0_ |= 0x00000004;
+         }
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public java.util.List<com.google.protobuf.ByteString>
+          getUrlList() {
+        return java.util.Collections.unmodifiableList(url_);
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public int getUrlCount() {
+        return url_.size();
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getUrl(int index) {
+        return url_.get(index);
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public Builder setUrl(
+          int index, com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureUrlIsMutable();
+        url_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public Builder addUrl(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureUrlIsMutable();
+        url_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public Builder addAllUrl(
+          java.lang.Iterable<? extends com.google.protobuf.ByteString> values) {
+        ensureUrlIsMutable();
+        super.addAll(values, url_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes url = 21;</code>
+       *
+       * <pre>
+       * URLs of the produced html reports
+       * </pre>
+       */
+      public Builder clearUrl() {
+        url_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000004);
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.SystraceReportMessage)
+    }
+
+    static {
+      defaultInstance = new SystraceReportMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.SystraceReportMessage)
+  }
+
+  public interface CoverageReportMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes file_path = 11;
+    /**
+     * <code>optional bytes file_path = 11;</code>
+     *
+     * <pre>
+     * the path to the source file from the project root.
+     * </pre>
+     */
+    boolean hasFilePath();
+    /**
+     * <code>optional bytes file_path = 11;</code>
+     *
+     * <pre>
+     * the path to the source file from the project root.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getFilePath();
+
+    // optional bytes project_name = 12;
+    /**
+     * <code>optional bytes project_name = 12;</code>
+     *
+     * <pre>
+     * the name of the project where the file can be found
+     * </pre>
+     */
+    boolean hasProjectName();
+    /**
+     * <code>optional bytes project_name = 12;</code>
+     *
+     * <pre>
+     * the name of the project where the file can be found
+     * </pre>
+     */
+    com.google.protobuf.ByteString getProjectName();
+
+    // optional bytes revision = 13;
+    /**
+     * <code>optional bytes revision = 13;</code>
+     *
+     * <pre>
+     * the commit ID identifying the code revision
+     * </pre>
+     */
+    boolean hasRevision();
+    /**
+     * <code>optional bytes revision = 13;</code>
+     *
+     * <pre>
+     * the commit ID identifying the code revision
+     * </pre>
+     */
+    com.google.protobuf.ByteString getRevision();
+
+    // repeated int32 line_coverage_vector = 23;
+    /**
+     * <code>repeated int32 line_coverage_vector = 23;</code>
+     *
+     * <pre>
+     * i-th element gives the number of times i-th line is executed.
+     * </pre>
+     */
+    java.util.List<java.lang.Integer> getLineCoverageVectorList();
+    /**
+     * <code>repeated int32 line_coverage_vector = 23;</code>
+     *
+     * <pre>
+     * i-th element gives the number of times i-th line is executed.
+     * </pre>
+     */
+    int getLineCoverageVectorCount();
+    /**
+     * <code>repeated int32 line_coverage_vector = 23;</code>
+     *
+     * <pre>
+     * i-th element gives the number of times i-th line is executed.
+     * </pre>
+     */
+    int getLineCoverageVector(int index);
+
+    // optional int32 total_line_count = 101;
+    /**
+     * <code>optional int32 total_line_count = 101;</code>
+     *
+     * <pre>
+     * the number of source code lines that are instrumented for code coverage
+     * measurement.
+     * </pre>
+     */
+    boolean hasTotalLineCount();
+    /**
+     * <code>optional int32 total_line_count = 101;</code>
+     *
+     * <pre>
+     * the number of source code lines that are instrumented for code coverage
+     * measurement.
+     * </pre>
+     */
+    int getTotalLineCount();
+
+    // optional int32 covered_line_count = 102;
+    /**
+     * <code>optional int32 covered_line_count = 102;</code>
+     *
+     * <pre>
+     * the number of source code lines that are executed.
+     * </pre>
+     */
+    boolean hasCoveredLineCount();
+    /**
+     * <code>optional int32 covered_line_count = 102;</code>
+     *
+     * <pre>
+     * the number of source code lines that are executed.
+     * </pre>
+     */
+    int getCoveredLineCount();
+
+    // optional bytes dir_path = 1 [deprecated = true];
+    /**
+     * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the directory path of a source file.
+     * </pre>
+     */
+    @java.lang.Deprecated boolean hasDirPath();
+    /**
+     * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the directory path of a source file.
+     * </pre>
+     */
+    @java.lang.Deprecated com.google.protobuf.ByteString getDirPath();
+
+    // optional bytes file_name = 2 [deprecated = true];
+    /**
+     * <code>optional bytes file_name = 2 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the name of the source file.
+     * </pre>
+     */
+    @java.lang.Deprecated boolean hasFileName();
+    /**
+     * <code>optional bytes file_name = 2 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the name of the source file.
+     * </pre>
+     */
+    @java.lang.Deprecated com.google.protobuf.ByteString getFileName();
+
+    // optional bytes html = 3 [deprecated = true];
+    /**
+     * <code>optional bytes html = 3 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * produced html report.
+     * </pre>
+     */
+    @java.lang.Deprecated boolean hasHtml();
+    /**
+     * <code>optional bytes html = 3 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * produced html report.
+     * </pre>
+     */
+    @java.lang.Deprecated com.google.protobuf.ByteString getHtml();
+  }
+  /**
+   * Protobuf type {@code android.vts.CoverageReportMessage}
+   *
+   * <pre>
+   * To specify a coverage report.
+   * </pre>
+   */
+  public static final class CoverageReportMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements CoverageReportMessageOrBuilder {
+    // Use CoverageReportMessage.newBuilder() to construct.
+    private CoverageReportMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private CoverageReportMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final CoverageReportMessage defaultInstance;
+    public static CoverageReportMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public CoverageReportMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private CoverageReportMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000020;
+              dirPath_ = input.readBytes();
+              break;
+            }
+            case 18: {
+              bitField0_ |= 0x00000040;
+              fileName_ = input.readBytes();
+              break;
+            }
+            case 26: {
+              bitField0_ |= 0x00000080;
+              html_ = input.readBytes();
+              break;
+            }
+            case 90: {
+              bitField0_ |= 0x00000001;
+              filePath_ = input.readBytes();
+              break;
+            }
+            case 98: {
+              bitField0_ |= 0x00000002;
+              projectName_ = input.readBytes();
+              break;
+            }
+            case 106: {
+              bitField0_ |= 0x00000004;
+              revision_ = input.readBytes();
+              break;
+            }
+            case 184: {
+              if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+                lineCoverageVector_ = new java.util.ArrayList<java.lang.Integer>();
+                mutable_bitField0_ |= 0x00000008;
+              }
+              lineCoverageVector_.add(input.readInt32());
+              break;
+            }
+            case 186: {
+              int length = input.readRawVarint32();
+              int limit = input.pushLimit(length);
+              if (!((mutable_bitField0_ & 0x00000008) == 0x00000008) && input.getBytesUntilLimit() > 0) {
+                lineCoverageVector_ = new java.util.ArrayList<java.lang.Integer>();
+                mutable_bitField0_ |= 0x00000008;
+              }
+              while (input.getBytesUntilLimit() > 0) {
+                lineCoverageVector_.add(input.readInt32());
+              }
+              input.popLimit(limit);
+              break;
+            }
+            case 808: {
+              bitField0_ |= 0x00000008;
+              totalLineCount_ = input.readInt32();
+              break;
+            }
+            case 816: {
+              bitField0_ |= 0x00000010;
+              coveredLineCount_ = input.readInt32();
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+          lineCoverageVector_ = java.util.Collections.unmodifiableList(lineCoverageVector_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_CoverageReportMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_CoverageReportMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.CoverageReportMessage.class, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<CoverageReportMessage> PARSER =
+        new com.google.protobuf.AbstractParser<CoverageReportMessage>() {
+      public CoverageReportMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new CoverageReportMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<CoverageReportMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes file_path = 11;
+    public static final int FILE_PATH_FIELD_NUMBER = 11;
+    private com.google.protobuf.ByteString filePath_;
+    /**
+     * <code>optional bytes file_path = 11;</code>
+     *
+     * <pre>
+     * the path to the source file from the project root.
+     * </pre>
+     */
+    public boolean hasFilePath() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes file_path = 11;</code>
+     *
+     * <pre>
+     * the path to the source file from the project root.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getFilePath() {
+      return filePath_;
+    }
+
+    // optional bytes project_name = 12;
+    public static final int PROJECT_NAME_FIELD_NUMBER = 12;
+    private com.google.protobuf.ByteString projectName_;
+    /**
+     * <code>optional bytes project_name = 12;</code>
+     *
+     * <pre>
+     * the name of the project where the file can be found
+     * </pre>
+     */
+    public boolean hasProjectName() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional bytes project_name = 12;</code>
+     *
+     * <pre>
+     * the name of the project where the file can be found
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getProjectName() {
+      return projectName_;
+    }
+
+    // optional bytes revision = 13;
+    public static final int REVISION_FIELD_NUMBER = 13;
+    private com.google.protobuf.ByteString revision_;
+    /**
+     * <code>optional bytes revision = 13;</code>
+     *
+     * <pre>
+     * the commit ID identifying the code revision
+     * </pre>
+     */
+    public boolean hasRevision() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional bytes revision = 13;</code>
+     *
+     * <pre>
+     * the commit ID identifying the code revision
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getRevision() {
+      return revision_;
+    }
+
+    // repeated int32 line_coverage_vector = 23;
+    public static final int LINE_COVERAGE_VECTOR_FIELD_NUMBER = 23;
+    private java.util.List<java.lang.Integer> lineCoverageVector_;
+    /**
+     * <code>repeated int32 line_coverage_vector = 23;</code>
+     *
+     * <pre>
+     * i-th element gives the number of times i-th line is executed.
+     * </pre>
+     */
+    public java.util.List<java.lang.Integer>
+        getLineCoverageVectorList() {
+      return lineCoverageVector_;
+    }
+    /**
+     * <code>repeated int32 line_coverage_vector = 23;</code>
+     *
+     * <pre>
+     * i-th element gives the number of times i-th line is executed.
+     * </pre>
+     */
+    public int getLineCoverageVectorCount() {
+      return lineCoverageVector_.size();
+    }
+    /**
+     * <code>repeated int32 line_coverage_vector = 23;</code>
+     *
+     * <pre>
+     * i-th element gives the number of times i-th line is executed.
+     * </pre>
+     */
+    public int getLineCoverageVector(int index) {
+      return lineCoverageVector_.get(index);
+    }
+
+    // optional int32 total_line_count = 101;
+    public static final int TOTAL_LINE_COUNT_FIELD_NUMBER = 101;
+    private int totalLineCount_;
+    /**
+     * <code>optional int32 total_line_count = 101;</code>
+     *
+     * <pre>
+     * the number of source code lines that are instrumented for code coverage
+     * measurement.
+     * </pre>
+     */
+    public boolean hasTotalLineCount() {
+      return ((bitField0_ & 0x00000008) == 0x00000008);
+    }
+    /**
+     * <code>optional int32 total_line_count = 101;</code>
+     *
+     * <pre>
+     * the number of source code lines that are instrumented for code coverage
+     * measurement.
+     * </pre>
+     */
+    public int getTotalLineCount() {
+      return totalLineCount_;
+    }
+
+    // optional int32 covered_line_count = 102;
+    public static final int COVERED_LINE_COUNT_FIELD_NUMBER = 102;
+    private int coveredLineCount_;
+    /**
+     * <code>optional int32 covered_line_count = 102;</code>
+     *
+     * <pre>
+     * the number of source code lines that are executed.
+     * </pre>
+     */
+    public boolean hasCoveredLineCount() {
+      return ((bitField0_ & 0x00000010) == 0x00000010);
+    }
+    /**
+     * <code>optional int32 covered_line_count = 102;</code>
+     *
+     * <pre>
+     * the number of source code lines that are executed.
+     * </pre>
+     */
+    public int getCoveredLineCount() {
+      return coveredLineCount_;
+    }
+
+    // optional bytes dir_path = 1 [deprecated = true];
+    public static final int DIR_PATH_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString dirPath_;
+    /**
+     * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the directory path of a source file.
+     * </pre>
+     */
+    @java.lang.Deprecated public boolean hasDirPath() {
+      return ((bitField0_ & 0x00000020) == 0x00000020);
+    }
+    /**
+     * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the directory path of a source file.
+     * </pre>
+     */
+    @java.lang.Deprecated public com.google.protobuf.ByteString getDirPath() {
+      return dirPath_;
+    }
+
+    // optional bytes file_name = 2 [deprecated = true];
+    public static final int FILE_NAME_FIELD_NUMBER = 2;
+    private com.google.protobuf.ByteString fileName_;
+    /**
+     * <code>optional bytes file_name = 2 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the name of the source file.
+     * </pre>
+     */
+    @java.lang.Deprecated public boolean hasFileName() {
+      return ((bitField0_ & 0x00000040) == 0x00000040);
+    }
+    /**
+     * <code>optional bytes file_name = 2 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * the name of the source file.
+     * </pre>
+     */
+    @java.lang.Deprecated public com.google.protobuf.ByteString getFileName() {
+      return fileName_;
+    }
+
+    // optional bytes html = 3 [deprecated = true];
+    public static final int HTML_FIELD_NUMBER = 3;
+    private com.google.protobuf.ByteString html_;
+    /**
+     * <code>optional bytes html = 3 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * produced html report.
+     * </pre>
+     */
+    @java.lang.Deprecated public boolean hasHtml() {
+      return ((bitField0_ & 0x00000080) == 0x00000080);
+    }
+    /**
+     * <code>optional bytes html = 3 [deprecated = true];</code>
+     *
+     * <pre>
+     * TODO(ryanjcampbell@) delete deprecated field
+     * produced html report.
+     * </pre>
+     */
+    @java.lang.Deprecated public com.google.protobuf.ByteString getHtml() {
+      return html_;
+    }
+
+    private void initFields() {
+      filePath_ = com.google.protobuf.ByteString.EMPTY;
+      projectName_ = com.google.protobuf.ByteString.EMPTY;
+      revision_ = com.google.protobuf.ByteString.EMPTY;
+      lineCoverageVector_ = java.util.Collections.emptyList();
+      totalLineCount_ = 0;
+      coveredLineCount_ = 0;
+      dirPath_ = com.google.protobuf.ByteString.EMPTY;
+      fileName_ = com.google.protobuf.ByteString.EMPTY;
+      html_ = com.google.protobuf.ByteString.EMPTY;
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        output.writeBytes(1, dirPath_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        output.writeBytes(2, fileName_);
+      }
+      if (((bitField0_ & 0x00000080) == 0x00000080)) {
+        output.writeBytes(3, html_);
+      }
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(11, filePath_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeBytes(12, projectName_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeBytes(13, revision_);
+      }
+      for (int i = 0; i < lineCoverageVector_.size(); i++) {
+        output.writeInt32(23, lineCoverageVector_.get(i));
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        output.writeInt32(101, totalLineCount_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        output.writeInt32(102, coveredLineCount_);
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, dirPath_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(2, fileName_);
+      }
+      if (((bitField0_ & 0x00000080) == 0x00000080)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(3, html_);
+      }
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(11, filePath_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(12, projectName_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(13, revision_);
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < lineCoverageVector_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeInt32SizeNoTag(lineCoverageVector_.get(i));
+        }
+        size += dataSize;
+        size += 2 * getLineCoverageVectorList().size();
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt32Size(101, totalLineCount_);
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt32Size(102, coveredLineCount_);
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.CoverageReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.CoverageReportMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.CoverageReportMessage}
+     *
+     * <pre>
+     * To specify a coverage report.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_CoverageReportMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_CoverageReportMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.CoverageReportMessage.class, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.CoverageReportMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        filePath_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        projectName_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        revision_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        lineCoverageVector_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000008);
+        totalLineCount_ = 0;
+        bitField0_ = (bitField0_ & ~0x00000010);
+        coveredLineCount_ = 0;
+        bitField0_ = (bitField0_ & ~0x00000020);
+        dirPath_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000040);
+        fileName_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000080);
+        html_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000100);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_CoverageReportMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.CoverageReportMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage build() {
+        com.android.vts.proto.VtsReportMessage.CoverageReportMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.CoverageReportMessage result = new com.android.vts.proto.VtsReportMessage.CoverageReportMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.filePath_ = filePath_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.projectName_ = projectName_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.revision_ = revision_;
+        if (((bitField0_ & 0x00000008) == 0x00000008)) {
+          lineCoverageVector_ = java.util.Collections.unmodifiableList(lineCoverageVector_);
+          bitField0_ = (bitField0_ & ~0x00000008);
+        }
+        result.lineCoverageVector_ = lineCoverageVector_;
+        if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
+          to_bitField0_ |= 0x00000008;
+        }
+        result.totalLineCount_ = totalLineCount_;
+        if (((from_bitField0_ & 0x00000020) == 0x00000020)) {
+          to_bitField0_ |= 0x00000010;
+        }
+        result.coveredLineCount_ = coveredLineCount_;
+        if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
+          to_bitField0_ |= 0x00000020;
+        }
+        result.dirPath_ = dirPath_;
+        if (((from_bitField0_ & 0x00000080) == 0x00000080)) {
+          to_bitField0_ |= 0x00000040;
+        }
+        result.fileName_ = fileName_;
+        if (((from_bitField0_ & 0x00000100) == 0x00000100)) {
+          to_bitField0_ |= 0x00000080;
+        }
+        result.html_ = html_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.CoverageReportMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.CoverageReportMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.CoverageReportMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.CoverageReportMessage.getDefaultInstance()) return this;
+        if (other.hasFilePath()) {
+          setFilePath(other.getFilePath());
+        }
+        if (other.hasProjectName()) {
+          setProjectName(other.getProjectName());
+        }
+        if (other.hasRevision()) {
+          setRevision(other.getRevision());
+        }
+        if (!other.lineCoverageVector_.isEmpty()) {
+          if (lineCoverageVector_.isEmpty()) {
+            lineCoverageVector_ = other.lineCoverageVector_;
+            bitField0_ = (bitField0_ & ~0x00000008);
+          } else {
+            ensureLineCoverageVectorIsMutable();
+            lineCoverageVector_.addAll(other.lineCoverageVector_);
+          }
+          onChanged();
+        }
+        if (other.hasTotalLineCount()) {
+          setTotalLineCount(other.getTotalLineCount());
+        }
+        if (other.hasCoveredLineCount()) {
+          setCoveredLineCount(other.getCoveredLineCount());
+        }
+        if (other.hasDirPath()) {
+          setDirPath(other.getDirPath());
+        }
+        if (other.hasFileName()) {
+          setFileName(other.getFileName());
+        }
+        if (other.hasHtml()) {
+          setHtml(other.getHtml());
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.CoverageReportMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.CoverageReportMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes file_path = 11;
+      private com.google.protobuf.ByteString filePath_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes file_path = 11;</code>
+       *
+       * <pre>
+       * the path to the source file from the project root.
+       * </pre>
+       */
+      public boolean hasFilePath() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes file_path = 11;</code>
+       *
+       * <pre>
+       * the path to the source file from the project root.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getFilePath() {
+        return filePath_;
+      }
+      /**
+       * <code>optional bytes file_path = 11;</code>
+       *
+       * <pre>
+       * the path to the source file from the project root.
+       * </pre>
+       */
+      public Builder setFilePath(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        filePath_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes file_path = 11;</code>
+       *
+       * <pre>
+       * the path to the source file from the project root.
+       * </pre>
+       */
+      public Builder clearFilePath() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        filePath_ = getDefaultInstance().getFilePath();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes project_name = 12;
+      private com.google.protobuf.ByteString projectName_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes project_name = 12;</code>
+       *
+       * <pre>
+       * the name of the project where the file can be found
+       * </pre>
+       */
+      public boolean hasProjectName() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional bytes project_name = 12;</code>
+       *
+       * <pre>
+       * the name of the project where the file can be found
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getProjectName() {
+        return projectName_;
+      }
+      /**
+       * <code>optional bytes project_name = 12;</code>
+       *
+       * <pre>
+       * the name of the project where the file can be found
+       * </pre>
+       */
+      public Builder setProjectName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000002;
+        projectName_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes project_name = 12;</code>
+       *
+       * <pre>
+       * the name of the project where the file can be found
+       * </pre>
+       */
+      public Builder clearProjectName() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        projectName_ = getDefaultInstance().getProjectName();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes revision = 13;
+      private com.google.protobuf.ByteString revision_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes revision = 13;</code>
+       *
+       * <pre>
+       * the commit ID identifying the code revision
+       * </pre>
+       */
+      public boolean hasRevision() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional bytes revision = 13;</code>
+       *
+       * <pre>
+       * the commit ID identifying the code revision
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getRevision() {
+        return revision_;
+      }
+      /**
+       * <code>optional bytes revision = 13;</code>
+       *
+       * <pre>
+       * the commit ID identifying the code revision
+       * </pre>
+       */
+      public Builder setRevision(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        revision_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes revision = 13;</code>
+       *
+       * <pre>
+       * the commit ID identifying the code revision
+       * </pre>
+       */
+      public Builder clearRevision() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        revision_ = getDefaultInstance().getRevision();
+        onChanged();
+        return this;
+      }
+
+      // repeated int32 line_coverage_vector = 23;
+      private java.util.List<java.lang.Integer> lineCoverageVector_ = java.util.Collections.emptyList();
+      private void ensureLineCoverageVectorIsMutable() {
+        if (!((bitField0_ & 0x00000008) == 0x00000008)) {
+          lineCoverageVector_ = new java.util.ArrayList<java.lang.Integer>(lineCoverageVector_);
+          bitField0_ |= 0x00000008;
+         }
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public java.util.List<java.lang.Integer>
+          getLineCoverageVectorList() {
+        return java.util.Collections.unmodifiableList(lineCoverageVector_);
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public int getLineCoverageVectorCount() {
+        return lineCoverageVector_.size();
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public int getLineCoverageVector(int index) {
+        return lineCoverageVector_.get(index);
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public Builder setLineCoverageVector(
+          int index, int value) {
+        ensureLineCoverageVectorIsMutable();
+        lineCoverageVector_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public Builder addLineCoverageVector(int value) {
+        ensureLineCoverageVectorIsMutable();
+        lineCoverageVector_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public Builder addAllLineCoverageVector(
+          java.lang.Iterable<? extends java.lang.Integer> values) {
+        ensureLineCoverageVectorIsMutable();
+        super.addAll(values, lineCoverageVector_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int32 line_coverage_vector = 23;</code>
+       *
+       * <pre>
+       * i-th element gives the number of times i-th line is executed.
+       * </pre>
+       */
+      public Builder clearLineCoverageVector() {
+        lineCoverageVector_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000008);
+        onChanged();
+        return this;
+      }
+
+      // optional int32 total_line_count = 101;
+      private int totalLineCount_ ;
+      /**
+       * <code>optional int32 total_line_count = 101;</code>
+       *
+       * <pre>
+       * the number of source code lines that are instrumented for code coverage
+       * measurement.
+       * </pre>
+       */
+      public boolean hasTotalLineCount() {
+        return ((bitField0_ & 0x00000010) == 0x00000010);
+      }
+      /**
+       * <code>optional int32 total_line_count = 101;</code>
+       *
+       * <pre>
+       * the number of source code lines that are instrumented for code coverage
+       * measurement.
+       * </pre>
+       */
+      public int getTotalLineCount() {
+        return totalLineCount_;
+      }
+      /**
+       * <code>optional int32 total_line_count = 101;</code>
+       *
+       * <pre>
+       * the number of source code lines that are instrumented for code coverage
+       * measurement.
+       * </pre>
+       */
+      public Builder setTotalLineCount(int value) {
+        bitField0_ |= 0x00000010;
+        totalLineCount_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int32 total_line_count = 101;</code>
+       *
+       * <pre>
+       * the number of source code lines that are instrumented for code coverage
+       * measurement.
+       * </pre>
+       */
+      public Builder clearTotalLineCount() {
+        bitField0_ = (bitField0_ & ~0x00000010);
+        totalLineCount_ = 0;
+        onChanged();
+        return this;
+      }
+
+      // optional int32 covered_line_count = 102;
+      private int coveredLineCount_ ;
+      /**
+       * <code>optional int32 covered_line_count = 102;</code>
+       *
+       * <pre>
+       * the number of source code lines that are executed.
+       * </pre>
+       */
+      public boolean hasCoveredLineCount() {
+        return ((bitField0_ & 0x00000020) == 0x00000020);
+      }
+      /**
+       * <code>optional int32 covered_line_count = 102;</code>
+       *
+       * <pre>
+       * the number of source code lines that are executed.
+       * </pre>
+       */
+      public int getCoveredLineCount() {
+        return coveredLineCount_;
+      }
+      /**
+       * <code>optional int32 covered_line_count = 102;</code>
+       *
+       * <pre>
+       * the number of source code lines that are executed.
+       * </pre>
+       */
+      public Builder setCoveredLineCount(int value) {
+        bitField0_ |= 0x00000020;
+        coveredLineCount_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int32 covered_line_count = 102;</code>
+       *
+       * <pre>
+       * the number of source code lines that are executed.
+       * </pre>
+       */
+      public Builder clearCoveredLineCount() {
+        bitField0_ = (bitField0_ & ~0x00000020);
+        coveredLineCount_ = 0;
+        onChanged();
+        return this;
+      }
+
+      // optional bytes dir_path = 1 [deprecated = true];
+      private com.google.protobuf.ByteString dirPath_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the directory path of a source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public boolean hasDirPath() {
+        return ((bitField0_ & 0x00000040) == 0x00000040);
+      }
+      /**
+       * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the directory path of a source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public com.google.protobuf.ByteString getDirPath() {
+        return dirPath_;
+      }
+      /**
+       * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the directory path of a source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder setDirPath(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000040;
+        dirPath_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes dir_path = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the directory path of a source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder clearDirPath() {
+        bitField0_ = (bitField0_ & ~0x00000040);
+        dirPath_ = getDefaultInstance().getDirPath();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes file_name = 2 [deprecated = true];
+      private com.google.protobuf.ByteString fileName_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes file_name = 2 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the name of the source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public boolean hasFileName() {
+        return ((bitField0_ & 0x00000080) == 0x00000080);
+      }
+      /**
+       * <code>optional bytes file_name = 2 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the name of the source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public com.google.protobuf.ByteString getFileName() {
+        return fileName_;
+      }
+      /**
+       * <code>optional bytes file_name = 2 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the name of the source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder setFileName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000080;
+        fileName_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes file_name = 2 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * the name of the source file.
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder clearFileName() {
+        bitField0_ = (bitField0_ & ~0x00000080);
+        fileName_ = getDefaultInstance().getFileName();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes html = 3 [deprecated = true];
+      private com.google.protobuf.ByteString html_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes html = 3 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * produced html report.
+       * </pre>
+       */
+      @java.lang.Deprecated public boolean hasHtml() {
+        return ((bitField0_ & 0x00000100) == 0x00000100);
+      }
+      /**
+       * <code>optional bytes html = 3 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * produced html report.
+       * </pre>
+       */
+      @java.lang.Deprecated public com.google.protobuf.ByteString getHtml() {
+        return html_;
+      }
+      /**
+       * <code>optional bytes html = 3 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * produced html report.
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder setHtml(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000100;
+        html_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes html = 3 [deprecated = true];</code>
+       *
+       * <pre>
+       * TODO(ryanjcampbell@) delete deprecated field
+       * produced html report.
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder clearHtml() {
+        bitField0_ = (bitField0_ & ~0x00000100);
+        html_ = getDefaultInstance().getHtml();
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.CoverageReportMessage)
+    }
+
+    static {
+      defaultInstance = new CoverageReportMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.CoverageReportMessage)
+  }
+
+  public interface LogMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes url = 1;
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a produced log file (e.g., stdout, stderr).
+     * </pre>
+     */
+    boolean hasUrl();
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a produced log file (e.g., stdout, stderr).
+     * </pre>
+     */
+    com.google.protobuf.ByteString getUrl();
+
+    // optional bytes name = 2;
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a log file.
+     * </pre>
+     */
+    boolean hasName();
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a log file.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getName();
+
+    // optional bytes content = 3;
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Content of log. Caution: do not put too much log in protobuf message,
+     * as BigTable for example recommends &lt; 10 MB for each record cell.
+     * </pre>
+     */
+    boolean hasContent();
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Content of log. Caution: do not put too much log in protobuf message,
+     * as BigTable for example recommends &lt; 10 MB for each record cell.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getContent();
+  }
+  /**
+   * Protobuf type {@code android.vts.LogMessage}
+   *
+   * <pre>
+   * To specify log report. This can be used either for per-test-module
+   * log message or per-test-case log message.
+   * </pre>
+   */
+  public static final class LogMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements LogMessageOrBuilder {
+    // Use LogMessage.newBuilder() to construct.
+    private LogMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private LogMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final LogMessage defaultInstance;
+    public static LogMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public LogMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private LogMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              url_ = input.readBytes();
+              break;
+            }
+            case 18: {
+              bitField0_ |= 0x00000002;
+              name_ = input.readBytes();
+              break;
+            }
+            case 26: {
+              bitField0_ |= 0x00000004;
+              content_ = input.readBytes();
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_LogMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_LogMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.LogMessage.class, com.android.vts.proto.VtsReportMessage.LogMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<LogMessage> PARSER =
+        new com.google.protobuf.AbstractParser<LogMessage>() {
+      public LogMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new LogMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<LogMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes url = 1;
+    public static final int URL_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString url_;
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a produced log file (e.g., stdout, stderr).
+     * </pre>
+     */
+    public boolean hasUrl() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a produced log file (e.g., stdout, stderr).
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getUrl() {
+      return url_;
+    }
+
+    // optional bytes name = 2;
+    public static final int NAME_FIELD_NUMBER = 2;
+    private com.google.protobuf.ByteString name_;
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a log file.
+     * </pre>
+     */
+    public boolean hasName() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a log file.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getName() {
+      return name_;
+    }
+
+    // optional bytes content = 3;
+    public static final int CONTENT_FIELD_NUMBER = 3;
+    private com.google.protobuf.ByteString content_;
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Content of log. Caution: do not put too much log in protobuf message,
+     * as BigTable for example recommends &lt; 10 MB for each record cell.
+     * </pre>
+     */
+    public boolean hasContent() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Content of log. Caution: do not put too much log in protobuf message,
+     * as BigTable for example recommends &lt; 10 MB for each record cell.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getContent() {
+      return content_;
+    }
+
+    private void initFields() {
+      url_ = com.google.protobuf.ByteString.EMPTY;
+      name_ = com.google.protobuf.ByteString.EMPTY;
+      content_ = com.google.protobuf.ByteString.EMPTY;
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, url_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeBytes(2, name_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeBytes(3, content_);
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, url_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(2, name_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(3, content_);
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.LogMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.LogMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.LogMessage}
+     *
+     * <pre>
+     * To specify log report. This can be used either for per-test-module
+     * log message or per-test-case log message.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_LogMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_LogMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.LogMessage.class, com.android.vts.proto.VtsReportMessage.LogMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.LogMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        url_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        name_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        content_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_LogMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.LogMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.LogMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.LogMessage build() {
+        com.android.vts.proto.VtsReportMessage.LogMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.LogMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.LogMessage result = new com.android.vts.proto.VtsReportMessage.LogMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.url_ = url_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.name_ = name_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.content_ = content_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.LogMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.LogMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.LogMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.LogMessage.getDefaultInstance()) return this;
+        if (other.hasUrl()) {
+          setUrl(other.getUrl());
+        }
+        if (other.hasName()) {
+          setName(other.getName());
+        }
+        if (other.hasContent()) {
+          setContent(other.getContent());
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.LogMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.LogMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes url = 1;
+      private com.google.protobuf.ByteString url_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a produced log file (e.g., stdout, stderr).
+       * </pre>
+       */
+      public boolean hasUrl() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a produced log file (e.g., stdout, stderr).
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getUrl() {
+        return url_;
+      }
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a produced log file (e.g., stdout, stderr).
+       * </pre>
+       */
+      public Builder setUrl(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        url_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a produced log file (e.g., stdout, stderr).
+       * </pre>
+       */
+      public Builder clearUrl() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        url_ = getDefaultInstance().getUrl();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes name = 2;
+      private com.google.protobuf.ByteString name_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a log file.
+       * </pre>
+       */
+      public boolean hasName() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a log file.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getName() {
+        return name_;
+      }
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a log file.
+       * </pre>
+       */
+      public Builder setName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000002;
+        name_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a log file.
+       * </pre>
+       */
+      public Builder clearName() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        name_ = getDefaultInstance().getName();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes content = 3;
+      private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Content of log. Caution: do not put too much log in protobuf message,
+       * as BigTable for example recommends &lt; 10 MB for each record cell.
+       * </pre>
+       */
+      public boolean hasContent() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Content of log. Caution: do not put too much log in protobuf message,
+       * as BigTable for example recommends &lt; 10 MB for each record cell.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getContent() {
+        return content_;
+      }
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Content of log. Caution: do not put too much log in protobuf message,
+       * as BigTable for example recommends &lt; 10 MB for each record cell.
+       * </pre>
+       */
+      public Builder setContent(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        content_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Content of log. Caution: do not put too much log in protobuf message,
+       * as BigTable for example recommends &lt; 10 MB for each record cell.
+       * </pre>
+       */
+      public Builder clearContent() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        content_ = getDefaultInstance().getContent();
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.LogMessage)
+    }
+
+    static {
+      defaultInstance = new LogMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.LogMessage)
+  }
+
+  public interface UrlResourceMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes url = 1;
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a resource file.
+     * </pre>
+     */
+    boolean hasUrl();
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a resource file.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getUrl();
+
+    // optional bytes name = 2;
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a resource file representing its type and does not have to be
+     * the same as the exact file name.
+     * </pre>
+     */
+    boolean hasName();
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a resource file representing its type and does not have to be
+     * the same as the exact file name.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getName();
+
+    // optional bytes content = 3;
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Raw content of a resource file. Used if the file is small.
+     * </pre>
+     */
+    boolean hasContent();
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Raw content of a resource file. Used if the file is small.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getContent();
+  }
+  /**
+   * Protobuf type {@code android.vts.UrlResourceMessage}
+   *
+   * <pre>
+   * To specify a resource object (reachable via a URL or contained in the
+   * message). This can be used to store a log file or an XML (or HTML) report
+   * file kept in a Google Cloud Storage (GCS) bucket or partner's network file
+   * system, or hosted by a HTTP server.
+   * </pre>
+   */
+  public static final class UrlResourceMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements UrlResourceMessageOrBuilder {
+    // Use UrlResourceMessage.newBuilder() to construct.
+    private UrlResourceMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private UrlResourceMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final UrlResourceMessage defaultInstance;
+    public static UrlResourceMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public UrlResourceMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private UrlResourceMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              url_ = input.readBytes();
+              break;
+            }
+            case 18: {
+              bitField0_ |= 0x00000002;
+              name_ = input.readBytes();
+              break;
+            }
+            case 26: {
+              bitField0_ |= 0x00000004;
+              content_ = input.readBytes();
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_UrlResourceMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_UrlResourceMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.UrlResourceMessage.class, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<UrlResourceMessage> PARSER =
+        new com.google.protobuf.AbstractParser<UrlResourceMessage>() {
+      public UrlResourceMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new UrlResourceMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<UrlResourceMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes url = 1;
+    public static final int URL_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString url_;
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a resource file.
+     * </pre>
+     */
+    public boolean hasUrl() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes url = 1;</code>
+     *
+     * <pre>
+     * URL of a resource file.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getUrl() {
+      return url_;
+    }
+
+    // optional bytes name = 2;
+    public static final int NAME_FIELD_NUMBER = 2;
+    private com.google.protobuf.ByteString name_;
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a resource file representing its type and does not have to be
+     * the same as the exact file name.
+     * </pre>
+     */
+    public boolean hasName() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional bytes name = 2;</code>
+     *
+     * <pre>
+     * Name of a resource file representing its type and does not have to be
+     * the same as the exact file name.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getName() {
+      return name_;
+    }
+
+    // optional bytes content = 3;
+    public static final int CONTENT_FIELD_NUMBER = 3;
+    private com.google.protobuf.ByteString content_;
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Raw content of a resource file. Used if the file is small.
+     * </pre>
+     */
+    public boolean hasContent() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional bytes content = 3;</code>
+     *
+     * <pre>
+     * Raw content of a resource file. Used if the file is small.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getContent() {
+      return content_;
+    }
+
+    private void initFields() {
+      url_ = com.google.protobuf.ByteString.EMPTY;
+      name_ = com.google.protobuf.ByteString.EMPTY;
+      content_ = com.google.protobuf.ByteString.EMPTY;
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, url_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeBytes(2, name_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeBytes(3, content_);
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, url_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(2, name_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(3, content_);
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.UrlResourceMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.UrlResourceMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.UrlResourceMessage}
+     *
+     * <pre>
+     * To specify a resource object (reachable via a URL or contained in the
+     * message). This can be used to store a log file or an XML (or HTML) report
+     * file kept in a Google Cloud Storage (GCS) bucket or partner's network file
+     * system, or hosted by a HTTP server.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_UrlResourceMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_UrlResourceMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.UrlResourceMessage.class, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.UrlResourceMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        url_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        name_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        content_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_UrlResourceMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.UrlResourceMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage build() {
+        com.android.vts.proto.VtsReportMessage.UrlResourceMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.UrlResourceMessage result = new com.android.vts.proto.VtsReportMessage.UrlResourceMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.url_ = url_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.name_ = name_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.content_ = content_;
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.UrlResourceMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.UrlResourceMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.UrlResourceMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.UrlResourceMessage.getDefaultInstance()) return this;
+        if (other.hasUrl()) {
+          setUrl(other.getUrl());
+        }
+        if (other.hasName()) {
+          setName(other.getName());
+        }
+        if (other.hasContent()) {
+          setContent(other.getContent());
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.UrlResourceMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.UrlResourceMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes url = 1;
+      private com.google.protobuf.ByteString url_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a resource file.
+       * </pre>
+       */
+      public boolean hasUrl() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a resource file.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getUrl() {
+        return url_;
+      }
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a resource file.
+       * </pre>
+       */
+      public Builder setUrl(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        url_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes url = 1;</code>
+       *
+       * <pre>
+       * URL of a resource file.
+       * </pre>
+       */
+      public Builder clearUrl() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        url_ = getDefaultInstance().getUrl();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes name = 2;
+      private com.google.protobuf.ByteString name_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a resource file representing its type and does not have to be
+       * the same as the exact file name.
+       * </pre>
+       */
+      public boolean hasName() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a resource file representing its type and does not have to be
+       * the same as the exact file name.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getName() {
+        return name_;
+      }
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a resource file representing its type and does not have to be
+       * the same as the exact file name.
+       * </pre>
+       */
+      public Builder setName(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000002;
+        name_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes name = 2;</code>
+       *
+       * <pre>
+       * Name of a resource file representing its type and does not have to be
+       * the same as the exact file name.
+       * </pre>
+       */
+      public Builder clearName() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        name_ = getDefaultInstance().getName();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes content = 3;
+      private com.google.protobuf.ByteString content_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Raw content of a resource file. Used if the file is small.
+       * </pre>
+       */
+      public boolean hasContent() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Raw content of a resource file. Used if the file is small.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getContent() {
+        return content_;
+      }
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Raw content of a resource file. Used if the file is small.
+       * </pre>
+       */
+      public Builder setContent(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        content_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes content = 3;</code>
+       *
+       * <pre>
+       * Raw content of a resource file. Used if the file is small.
+       * </pre>
+       */
+      public Builder clearContent() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        content_ = getDefaultInstance().getContent();
+        onChanged();
+        return this;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.UrlResourceMessage)
+    }
+
+    static {
+      defaultInstance = new UrlResourceMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.UrlResourceMessage)
+  }
+
+  public interface TestReportMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional bytes test_suite = 1 [deprecated = true];
+    /**
+     * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * The test suite name..
+     * </pre>
+     */
+    @java.lang.Deprecated boolean hasTestSuite();
+    /**
+     * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * The test suite name..
+     * </pre>
+     */
+    @java.lang.Deprecated com.google.protobuf.ByteString getTestSuite();
+
+    // optional bytes test = 2;
+    /**
+     * <code>optional bytes test = 2;</code>
+     *
+     * <pre>
+     * The test name.
+     * </pre>
+     */
+    boolean hasTest();
+    /**
+     * <code>optional bytes test = 2;</code>
+     *
+     * <pre>
+     * The test name.
+     * </pre>
+     */
+    com.google.protobuf.ByteString getTest();
+
+    // optional .android.vts.VtsTestType test_type = 3;
+    /**
+     * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+     *
+     * <pre>
+     * The test type
+     * </pre>
+     */
+    boolean hasTestType();
+    /**
+     * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+     *
+     * <pre>
+     * The test type
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.VtsTestType getTestType();
+
+    // repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage> 
+        getDeviceInfoList();
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage getDeviceInfo(int index);
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    int getDeviceInfoCount();
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder> 
+        getDeviceInfoOrBuilderList();
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder getDeviceInfoOrBuilder(
+        int index);
+
+    // optional .android.vts.AndroidBuildInfo build_info = 5;
+    /**
+     * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+     *
+     * <pre>
+     * Build info
+     * </pre>
+     */
+    boolean hasBuildInfo();
+    /**
+     * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+     *
+     * <pre>
+     * Build info
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.AndroidBuildInfo getBuildInfo();
+    /**
+     * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+     *
+     * <pre>
+     * Build info
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder getBuildInfoOrBuilder();
+
+    // repeated bytes subscriber_email = 6;
+    /**
+     * <code>repeated bytes subscriber_email = 6;</code>
+     *
+     * <pre>
+     * Email addresses of subscribers to the test results
+     * </pre>
+     */
+    java.util.List<com.google.protobuf.ByteString> getSubscriberEmailList();
+    /**
+     * <code>repeated bytes subscriber_email = 6;</code>
+     *
+     * <pre>
+     * Email addresses of subscribers to the test results
+     * </pre>
+     */
+    int getSubscriberEmailCount();
+    /**
+     * <code>repeated bytes subscriber_email = 6;</code>
+     *
+     * <pre>
+     * Email addresses of subscribers to the test results
+     * </pre>
+     */
+    com.google.protobuf.ByteString getSubscriberEmail(int index);
+
+    // optional .android.vts.VtsHostInfo host_info = 7;
+    /**
+     * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+     *
+     * <pre>
+     * Info about the host computer
+     * </pre>
+     */
+    boolean hasHostInfo();
+    /**
+     * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+     *
+     * <pre>
+     * Info about the host computer
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.VtsHostInfo getHostInfo();
+    /**
+     * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+     *
+     * <pre>
+     * Info about the host computer
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder getHostInfoOrBuilder();
+
+    // repeated .android.vts.TestCaseReportMessage test_case = 11;
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage> 
+        getTestCaseList();
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.TestCaseReportMessage getTestCase(int index);
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    int getTestCaseCount();
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder> 
+        getTestCaseOrBuilderList();
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder getTestCaseOrBuilder(
+        int index);
+
+    // repeated .android.vts.ProfilingReportMessage profiling = 21;
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> 
+        getProfilingList();
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getProfiling(int index);
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    int getProfilingCount();
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+        getProfilingOrBuilderList();
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder getProfilingOrBuilder(
+        int index);
+
+    // repeated .android.vts.SystraceReportMessage systrace = 22;
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> 
+        getSystraceList();
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.SystraceReportMessage getSystrace(int index);
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    int getSystraceCount();
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+        getSystraceOrBuilderList();
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder getSystraceOrBuilder(
+        int index);
+
+    // optional int64 start_timestamp = 101;
+    /**
+     * <code>optional int64 start_timestamp = 101;</code>
+     *
+     * <pre>
+     * Execution start and end time stamp.
+     * </pre>
+     */
+    boolean hasStartTimestamp();
+    /**
+     * <code>optional int64 start_timestamp = 101;</code>
+     *
+     * <pre>
+     * Execution start and end time stamp.
+     * </pre>
+     */
+    long getStartTimestamp();
+
+    // optional int64 end_timestamp = 102;
+    /**
+     * <code>optional int64 end_timestamp = 102;</code>
+     */
+    boolean hasEndTimestamp();
+    /**
+     * <code>optional int64 end_timestamp = 102;</code>
+     */
+    long getEndTimestamp();
+
+    // repeated .android.vts.CoverageReportMessage coverage = 103;
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> 
+        getCoverageList();
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.CoverageReportMessage getCoverage(int index);
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    int getCoverageCount();
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+        getCoverageOrBuilderList();
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder getCoverageOrBuilder(
+        int index);
+
+    // repeated .android.vts.LogMessage log = 1001;
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> 
+        getLogList();
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.LogMessage getLog(int index);
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    int getLogCount();
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+        getLogOrBuilderList();
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder getLogOrBuilder(
+        int index);
+  }
+  /**
+   * Protobuf type {@code android.vts.TestReportMessage}
+   *
+   * <pre>
+   * To specify a test execution report.
+   * </pre>
+   */
+  public static final class TestReportMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements TestReportMessageOrBuilder {
+    // Use TestReportMessage.newBuilder() to construct.
+    private TestReportMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private TestReportMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final TestReportMessage defaultInstance;
+    public static TestReportMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public TestReportMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private TestReportMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              testSuite_ = input.readBytes();
+              break;
+            }
+            case 18: {
+              bitField0_ |= 0x00000002;
+              test_ = input.readBytes();
+              break;
+            }
+            case 24: {
+              int rawValue = input.readEnum();
+              com.android.vts.proto.VtsReportMessage.VtsTestType value = com.android.vts.proto.VtsReportMessage.VtsTestType.valueOf(rawValue);
+              if (value == null) {
+                unknownFields.mergeVarintField(3, rawValue);
+              } else {
+                bitField0_ |= 0x00000004;
+                testType_ = value;
+              }
+              break;
+            }
+            case 34: {
+              if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+                deviceInfo_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage>();
+                mutable_bitField0_ |= 0x00000008;
+              }
+              deviceInfo_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 42: {
+              com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder subBuilder = null;
+              if (((bitField0_ & 0x00000008) == 0x00000008)) {
+                subBuilder = buildInfo_.toBuilder();
+              }
+              buildInfo_ = input.readMessage(com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.PARSER, extensionRegistry);
+              if (subBuilder != null) {
+                subBuilder.mergeFrom(buildInfo_);
+                buildInfo_ = subBuilder.buildPartial();
+              }
+              bitField0_ |= 0x00000008;
+              break;
+            }
+            case 50: {
+              if (!((mutable_bitField0_ & 0x00000020) == 0x00000020)) {
+                subscriberEmail_ = new java.util.ArrayList<com.google.protobuf.ByteString>();
+                mutable_bitField0_ |= 0x00000020;
+              }
+              subscriberEmail_.add(input.readBytes());
+              break;
+            }
+            case 58: {
+              com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder subBuilder = null;
+              if (((bitField0_ & 0x00000010) == 0x00000010)) {
+                subBuilder = hostInfo_.toBuilder();
+              }
+              hostInfo_ = input.readMessage(com.android.vts.proto.VtsReportMessage.VtsHostInfo.PARSER, extensionRegistry);
+              if (subBuilder != null) {
+                subBuilder.mergeFrom(hostInfo_);
+                hostInfo_ = subBuilder.buildPartial();
+              }
+              bitField0_ |= 0x00000010;
+              break;
+            }
+            case 90: {
+              if (!((mutable_bitField0_ & 0x00000080) == 0x00000080)) {
+                testCase_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage>();
+                mutable_bitField0_ |= 0x00000080;
+              }
+              testCase_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 170: {
+              if (!((mutable_bitField0_ & 0x00000100) == 0x00000100)) {
+                profiling_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage>();
+                mutable_bitField0_ |= 0x00000100;
+              }
+              profiling_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 178: {
+              if (!((mutable_bitField0_ & 0x00000200) == 0x00000200)) {
+                systrace_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.SystraceReportMessage>();
+                mutable_bitField0_ |= 0x00000200;
+              }
+              systrace_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.SystraceReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 808: {
+              bitField0_ |= 0x00000020;
+              startTimestamp_ = input.readInt64();
+              break;
+            }
+            case 816: {
+              bitField0_ |= 0x00000040;
+              endTimestamp_ = input.readInt64();
+              break;
+            }
+            case 826: {
+              if (!((mutable_bitField0_ & 0x00001000) == 0x00001000)) {
+                coverage_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.CoverageReportMessage>();
+                mutable_bitField0_ |= 0x00001000;
+              }
+              coverage_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.CoverageReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 8010: {
+              if (!((mutable_bitField0_ & 0x00002000) == 0x00002000)) {
+                log_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.LogMessage>();
+                mutable_bitField0_ |= 0x00002000;
+              }
+              log_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.LogMessage.PARSER, extensionRegistry));
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+          deviceInfo_ = java.util.Collections.unmodifiableList(deviceInfo_);
+        }
+        if (((mutable_bitField0_ & 0x00000020) == 0x00000020)) {
+          subscriberEmail_ = java.util.Collections.unmodifiableList(subscriberEmail_);
+        }
+        if (((mutable_bitField0_ & 0x00000080) == 0x00000080)) {
+          testCase_ = java.util.Collections.unmodifiableList(testCase_);
+        }
+        if (((mutable_bitField0_ & 0x00000100) == 0x00000100)) {
+          profiling_ = java.util.Collections.unmodifiableList(profiling_);
+        }
+        if (((mutable_bitField0_ & 0x00000200) == 0x00000200)) {
+          systrace_ = java.util.Collections.unmodifiableList(systrace_);
+        }
+        if (((mutable_bitField0_ & 0x00001000) == 0x00001000)) {
+          coverage_ = java.util.Collections.unmodifiableList(coverage_);
+        }
+        if (((mutable_bitField0_ & 0x00002000) == 0x00002000)) {
+          log_ = java.util.Collections.unmodifiableList(log_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestReportMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestReportMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.TestReportMessage.class, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<TestReportMessage> PARSER =
+        new com.google.protobuf.AbstractParser<TestReportMessage>() {
+      public TestReportMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new TestReportMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<TestReportMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional bytes test_suite = 1 [deprecated = true];
+    public static final int TEST_SUITE_FIELD_NUMBER = 1;
+    private com.google.protobuf.ByteString testSuite_;
+    /**
+     * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * The test suite name..
+     * </pre>
+     */
+    @java.lang.Deprecated public boolean hasTestSuite() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+     *
+     * <pre>
+     * The test suite name..
+     * </pre>
+     */
+    @java.lang.Deprecated public com.google.protobuf.ByteString getTestSuite() {
+      return testSuite_;
+    }
+
+    // optional bytes test = 2;
+    public static final int TEST_FIELD_NUMBER = 2;
+    private com.google.protobuf.ByteString test_;
+    /**
+     * <code>optional bytes test = 2;</code>
+     *
+     * <pre>
+     * The test name.
+     * </pre>
+     */
+    public boolean hasTest() {
+      return ((bitField0_ & 0x00000002) == 0x00000002);
+    }
+    /**
+     * <code>optional bytes test = 2;</code>
+     *
+     * <pre>
+     * The test name.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getTest() {
+      return test_;
+    }
+
+    // optional .android.vts.VtsTestType test_type = 3;
+    public static final int TEST_TYPE_FIELD_NUMBER = 3;
+    private com.android.vts.proto.VtsReportMessage.VtsTestType testType_;
+    /**
+     * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+     *
+     * <pre>
+     * The test type
+     * </pre>
+     */
+    public boolean hasTestType() {
+      return ((bitField0_ & 0x00000004) == 0x00000004);
+    }
+    /**
+     * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+     *
+     * <pre>
+     * The test type
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.VtsTestType getTestType() {
+      return testType_;
+    }
+
+    // repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;
+    public static final int DEVICE_INFO_FIELD_NUMBER = 4;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage> deviceInfo_;
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage> getDeviceInfoList() {
+      return deviceInfo_;
+    }
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder> 
+        getDeviceInfoOrBuilderList() {
+      return deviceInfo_;
+    }
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    public int getDeviceInfoCount() {
+      return deviceInfo_.size();
+    }
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage getDeviceInfo(int index) {
+      return deviceInfo_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+     *
+     * <pre>
+     * Target device info
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder getDeviceInfoOrBuilder(
+        int index) {
+      return deviceInfo_.get(index);
+    }
+
+    // optional .android.vts.AndroidBuildInfo build_info = 5;
+    public static final int BUILD_INFO_FIELD_NUMBER = 5;
+    private com.android.vts.proto.VtsReportMessage.AndroidBuildInfo buildInfo_;
+    /**
+     * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+     *
+     * <pre>
+     * Build info
+     * </pre>
+     */
+    public boolean hasBuildInfo() {
+      return ((bitField0_ & 0x00000008) == 0x00000008);
+    }
+    /**
+     * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+     *
+     * <pre>
+     * Build info
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.AndroidBuildInfo getBuildInfo() {
+      return buildInfo_;
+    }
+    /**
+     * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+     *
+     * <pre>
+     * Build info
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder getBuildInfoOrBuilder() {
+      return buildInfo_;
+    }
+
+    // repeated bytes subscriber_email = 6;
+    public static final int SUBSCRIBER_EMAIL_FIELD_NUMBER = 6;
+    private java.util.List<com.google.protobuf.ByteString> subscriberEmail_;
+    /**
+     * <code>repeated bytes subscriber_email = 6;</code>
+     *
+     * <pre>
+     * Email addresses of subscribers to the test results
+     * </pre>
+     */
+    public java.util.List<com.google.protobuf.ByteString>
+        getSubscriberEmailList() {
+      return subscriberEmail_;
+    }
+    /**
+     * <code>repeated bytes subscriber_email = 6;</code>
+     *
+     * <pre>
+     * Email addresses of subscribers to the test results
+     * </pre>
+     */
+    public int getSubscriberEmailCount() {
+      return subscriberEmail_.size();
+    }
+    /**
+     * <code>repeated bytes subscriber_email = 6;</code>
+     *
+     * <pre>
+     * Email addresses of subscribers to the test results
+     * </pre>
+     */
+    public com.google.protobuf.ByteString getSubscriberEmail(int index) {
+      return subscriberEmail_.get(index);
+    }
+
+    // optional .android.vts.VtsHostInfo host_info = 7;
+    public static final int HOST_INFO_FIELD_NUMBER = 7;
+    private com.android.vts.proto.VtsReportMessage.VtsHostInfo hostInfo_;
+    /**
+     * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+     *
+     * <pre>
+     * Info about the host computer
+     * </pre>
+     */
+    public boolean hasHostInfo() {
+      return ((bitField0_ & 0x00000010) == 0x00000010);
+    }
+    /**
+     * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+     *
+     * <pre>
+     * Info about the host computer
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.VtsHostInfo getHostInfo() {
+      return hostInfo_;
+    }
+    /**
+     * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+     *
+     * <pre>
+     * Info about the host computer
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder getHostInfoOrBuilder() {
+      return hostInfo_;
+    }
+
+    // repeated .android.vts.TestCaseReportMessage test_case = 11;
+    public static final int TEST_CASE_FIELD_NUMBER = 11;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage> testCase_;
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage> getTestCaseList() {
+      return testCase_;
+    }
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder> 
+        getTestCaseOrBuilderList() {
+      return testCase_;
+    }
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    public int getTestCaseCount() {
+      return testCase_.size();
+    }
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage getTestCase(int index) {
+      return testCase_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+     *
+     * <pre>
+     * Test case reports
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder getTestCaseOrBuilder(
+        int index) {
+      return testCase_.get(index);
+    }
+
+    // repeated .android.vts.ProfilingReportMessage profiling = 21;
+    public static final int PROFILING_FIELD_NUMBER = 21;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> profiling_;
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> getProfilingList() {
+      return profiling_;
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+        getProfilingOrBuilderList() {
+      return profiling_;
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    public int getProfilingCount() {
+      return profiling_.size();
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getProfiling(int index) {
+      return profiling_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+     *
+     * <pre>
+     * Profiling reports
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder getProfilingOrBuilder(
+        int index) {
+      return profiling_.get(index);
+    }
+
+    // repeated .android.vts.SystraceReportMessage systrace = 22;
+    public static final int SYSTRACE_FIELD_NUMBER = 22;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> systrace_;
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> getSystraceList() {
+      return systrace_;
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+        getSystraceOrBuilderList() {
+      return systrace_;
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    public int getSystraceCount() {
+      return systrace_.size();
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.SystraceReportMessage getSystrace(int index) {
+      return systrace_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+     *
+     * <pre>
+     * Systrace report per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder getSystraceOrBuilder(
+        int index) {
+      return systrace_.get(index);
+    }
+
+    // optional int64 start_timestamp = 101;
+    public static final int START_TIMESTAMP_FIELD_NUMBER = 101;
+    private long startTimestamp_;
+    /**
+     * <code>optional int64 start_timestamp = 101;</code>
+     *
+     * <pre>
+     * Execution start and end time stamp.
+     * </pre>
+     */
+    public boolean hasStartTimestamp() {
+      return ((bitField0_ & 0x00000020) == 0x00000020);
+    }
+    /**
+     * <code>optional int64 start_timestamp = 101;</code>
+     *
+     * <pre>
+     * Execution start and end time stamp.
+     * </pre>
+     */
+    public long getStartTimestamp() {
+      return startTimestamp_;
+    }
+
+    // optional int64 end_timestamp = 102;
+    public static final int END_TIMESTAMP_FIELD_NUMBER = 102;
+    private long endTimestamp_;
+    /**
+     * <code>optional int64 end_timestamp = 102;</code>
+     */
+    public boolean hasEndTimestamp() {
+      return ((bitField0_ & 0x00000040) == 0x00000040);
+    }
+    /**
+     * <code>optional int64 end_timestamp = 102;</code>
+     */
+    public long getEndTimestamp() {
+      return endTimestamp_;
+    }
+
+    // repeated .android.vts.CoverageReportMessage coverage = 103;
+    public static final int COVERAGE_FIELD_NUMBER = 103;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> coverage_;
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> getCoverageList() {
+      return coverage_;
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+        getCoverageOrBuilderList() {
+      return coverage_;
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    public int getCoverageCount() {
+      return coverage_.size();
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.CoverageReportMessage getCoverage(int index) {
+      return coverage_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+     *
+     * <pre>
+     * Coverage report per file
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder getCoverageOrBuilder(
+        int index) {
+      return coverage_.get(index);
+    }
+
+    // repeated .android.vts.LogMessage log = 1001;
+    public static final int LOG_FIELD_NUMBER = 1001;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> log_;
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> getLogList() {
+      return log_;
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+        getLogOrBuilderList() {
+      return log_;
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public int getLogCount() {
+      return log_.size();
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.LogMessage getLog(int index) {
+      return log_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.LogMessage log = 1001;</code>
+     *
+     * <pre>
+     * Log for a test module. May contain multiple logs such as logcat, host log,
+     * etc.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder getLogOrBuilder(
+        int index) {
+      return log_.get(index);
+    }
+
+    private void initFields() {
+      testSuite_ = com.google.protobuf.ByteString.EMPTY;
+      test_ = com.google.protobuf.ByteString.EMPTY;
+      testType_ = com.android.vts.proto.VtsReportMessage.VtsTestType.UNKNOWN_VTS_TESTTYPE;
+      deviceInfo_ = java.util.Collections.emptyList();
+      buildInfo_ = com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance();
+      subscriberEmail_ = java.util.Collections.emptyList();
+      hostInfo_ = com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance();
+      testCase_ = java.util.Collections.emptyList();
+      profiling_ = java.util.Collections.emptyList();
+      systrace_ = java.util.Collections.emptyList();
+      startTimestamp_ = 0L;
+      endTimestamp_ = 0L;
+      coverage_ = java.util.Collections.emptyList();
+      log_ = java.util.Collections.emptyList();
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, testSuite_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        output.writeBytes(2, test_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        output.writeEnum(3, testType_.getNumber());
+      }
+      for (int i = 0; i < deviceInfo_.size(); i++) {
+        output.writeMessage(4, deviceInfo_.get(i));
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        output.writeMessage(5, buildInfo_);
+      }
+      for (int i = 0; i < subscriberEmail_.size(); i++) {
+        output.writeBytes(6, subscriberEmail_.get(i));
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        output.writeMessage(7, hostInfo_);
+      }
+      for (int i = 0; i < testCase_.size(); i++) {
+        output.writeMessage(11, testCase_.get(i));
+      }
+      for (int i = 0; i < profiling_.size(); i++) {
+        output.writeMessage(21, profiling_.get(i));
+      }
+      for (int i = 0; i < systrace_.size(); i++) {
+        output.writeMessage(22, systrace_.get(i));
+      }
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        output.writeInt64(101, startTimestamp_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        output.writeInt64(102, endTimestamp_);
+      }
+      for (int i = 0; i < coverage_.size(); i++) {
+        output.writeMessage(103, coverage_.get(i));
+      }
+      for (int i = 0; i < log_.size(); i++) {
+        output.writeMessage(1001, log_.get(i));
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, testSuite_);
+      }
+      if (((bitField0_ & 0x00000002) == 0x00000002)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(2, test_);
+      }
+      if (((bitField0_ & 0x00000004) == 0x00000004)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeEnumSize(3, testType_.getNumber());
+      }
+      for (int i = 0; i < deviceInfo_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(4, deviceInfo_.get(i));
+      }
+      if (((bitField0_ & 0x00000008) == 0x00000008)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(5, buildInfo_);
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < subscriberEmail_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeBytesSizeNoTag(subscriberEmail_.get(i));
+        }
+        size += dataSize;
+        size += 1 * getSubscriberEmailList().size();
+      }
+      if (((bitField0_ & 0x00000010) == 0x00000010)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(7, hostInfo_);
+      }
+      for (int i = 0; i < testCase_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(11, testCase_.get(i));
+      }
+      for (int i = 0; i < profiling_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(21, profiling_.get(i));
+      }
+      for (int i = 0; i < systrace_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(22, systrace_.get(i));
+      }
+      if (((bitField0_ & 0x00000020) == 0x00000020)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(101, startTimestamp_);
+      }
+      if (((bitField0_ & 0x00000040) == 0x00000040)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeInt64Size(102, endTimestamp_);
+      }
+      for (int i = 0; i < coverage_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(103, coverage_.get(i));
+      }
+      for (int i = 0; i < log_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(1001, log_.get(i));
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.TestReportMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.TestReportMessage}
+     *
+     * <pre>
+     * To specify a test execution report.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestReportMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestReportMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.TestReportMessage.class, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.TestReportMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+          getDeviceInfoFieldBuilder();
+          getBuildInfoFieldBuilder();
+          getHostInfoFieldBuilder();
+          getTestCaseFieldBuilder();
+          getProfilingFieldBuilder();
+          getSystraceFieldBuilder();
+          getCoverageFieldBuilder();
+          getLogFieldBuilder();
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        testSuite_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        test_ = com.google.protobuf.ByteString.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000002);
+        testType_ = com.android.vts.proto.VtsReportMessage.VtsTestType.UNKNOWN_VTS_TESTTYPE;
+        bitField0_ = (bitField0_ & ~0x00000004);
+        if (deviceInfoBuilder_ == null) {
+          deviceInfo_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000008);
+        } else {
+          deviceInfoBuilder_.clear();
+        }
+        if (buildInfoBuilder_ == null) {
+          buildInfo_ = com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance();
+        } else {
+          buildInfoBuilder_.clear();
+        }
+        bitField0_ = (bitField0_ & ~0x00000010);
+        subscriberEmail_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000020);
+        if (hostInfoBuilder_ == null) {
+          hostInfo_ = com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance();
+        } else {
+          hostInfoBuilder_.clear();
+        }
+        bitField0_ = (bitField0_ & ~0x00000040);
+        if (testCaseBuilder_ == null) {
+          testCase_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000080);
+        } else {
+          testCaseBuilder_.clear();
+        }
+        if (profilingBuilder_ == null) {
+          profiling_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000100);
+        } else {
+          profilingBuilder_.clear();
+        }
+        if (systraceBuilder_ == null) {
+          systrace_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000200);
+        } else {
+          systraceBuilder_.clear();
+        }
+        startTimestamp_ = 0L;
+        bitField0_ = (bitField0_ & ~0x00000400);
+        endTimestamp_ = 0L;
+        bitField0_ = (bitField0_ & ~0x00000800);
+        if (coverageBuilder_ == null) {
+          coverage_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00001000);
+        } else {
+          coverageBuilder_.clear();
+        }
+        if (logBuilder_ == null) {
+          log_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00002000);
+        } else {
+          logBuilder_.clear();
+        }
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestReportMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.TestReportMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage build() {
+        com.android.vts.proto.VtsReportMessage.TestReportMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.TestReportMessage result = new com.android.vts.proto.VtsReportMessage.TestReportMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.testSuite_ = testSuite_;
+        if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
+          to_bitField0_ |= 0x00000002;
+        }
+        result.test_ = test_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000004;
+        }
+        result.testType_ = testType_;
+        if (deviceInfoBuilder_ == null) {
+          if (((bitField0_ & 0x00000008) == 0x00000008)) {
+            deviceInfo_ = java.util.Collections.unmodifiableList(deviceInfo_);
+            bitField0_ = (bitField0_ & ~0x00000008);
+          }
+          result.deviceInfo_ = deviceInfo_;
+        } else {
+          result.deviceInfo_ = deviceInfoBuilder_.build();
+        }
+        if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
+          to_bitField0_ |= 0x00000008;
+        }
+        if (buildInfoBuilder_ == null) {
+          result.buildInfo_ = buildInfo_;
+        } else {
+          result.buildInfo_ = buildInfoBuilder_.build();
+        }
+        if (((bitField0_ & 0x00000020) == 0x00000020)) {
+          subscriberEmail_ = java.util.Collections.unmodifiableList(subscriberEmail_);
+          bitField0_ = (bitField0_ & ~0x00000020);
+        }
+        result.subscriberEmail_ = subscriberEmail_;
+        if (((from_bitField0_ & 0x00000040) == 0x00000040)) {
+          to_bitField0_ |= 0x00000010;
+        }
+        if (hostInfoBuilder_ == null) {
+          result.hostInfo_ = hostInfo_;
+        } else {
+          result.hostInfo_ = hostInfoBuilder_.build();
+        }
+        if (testCaseBuilder_ == null) {
+          if (((bitField0_ & 0x00000080) == 0x00000080)) {
+            testCase_ = java.util.Collections.unmodifiableList(testCase_);
+            bitField0_ = (bitField0_ & ~0x00000080);
+          }
+          result.testCase_ = testCase_;
+        } else {
+          result.testCase_ = testCaseBuilder_.build();
+        }
+        if (profilingBuilder_ == null) {
+          if (((bitField0_ & 0x00000100) == 0x00000100)) {
+            profiling_ = java.util.Collections.unmodifiableList(profiling_);
+            bitField0_ = (bitField0_ & ~0x00000100);
+          }
+          result.profiling_ = profiling_;
+        } else {
+          result.profiling_ = profilingBuilder_.build();
+        }
+        if (systraceBuilder_ == null) {
+          if (((bitField0_ & 0x00000200) == 0x00000200)) {
+            systrace_ = java.util.Collections.unmodifiableList(systrace_);
+            bitField0_ = (bitField0_ & ~0x00000200);
+          }
+          result.systrace_ = systrace_;
+        } else {
+          result.systrace_ = systraceBuilder_.build();
+        }
+        if (((from_bitField0_ & 0x00000400) == 0x00000400)) {
+          to_bitField0_ |= 0x00000020;
+        }
+        result.startTimestamp_ = startTimestamp_;
+        if (((from_bitField0_ & 0x00000800) == 0x00000800)) {
+          to_bitField0_ |= 0x00000040;
+        }
+        result.endTimestamp_ = endTimestamp_;
+        if (coverageBuilder_ == null) {
+          if (((bitField0_ & 0x00001000) == 0x00001000)) {
+            coverage_ = java.util.Collections.unmodifiableList(coverage_);
+            bitField0_ = (bitField0_ & ~0x00001000);
+          }
+          result.coverage_ = coverage_;
+        } else {
+          result.coverage_ = coverageBuilder_.build();
+        }
+        if (logBuilder_ == null) {
+          if (((bitField0_ & 0x00002000) == 0x00002000)) {
+            log_ = java.util.Collections.unmodifiableList(log_);
+            bitField0_ = (bitField0_ & ~0x00002000);
+          }
+          result.log_ = log_;
+        } else {
+          result.log_ = logBuilder_.build();
+        }
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.TestReportMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.TestReportMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.TestReportMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.TestReportMessage.getDefaultInstance()) return this;
+        if (other.hasTestSuite()) {
+          setTestSuite(other.getTestSuite());
+        }
+        if (other.hasTest()) {
+          setTest(other.getTest());
+        }
+        if (other.hasTestType()) {
+          setTestType(other.getTestType());
+        }
+        if (deviceInfoBuilder_ == null) {
+          if (!other.deviceInfo_.isEmpty()) {
+            if (deviceInfo_.isEmpty()) {
+              deviceInfo_ = other.deviceInfo_;
+              bitField0_ = (bitField0_ & ~0x00000008);
+            } else {
+              ensureDeviceInfoIsMutable();
+              deviceInfo_.addAll(other.deviceInfo_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.deviceInfo_.isEmpty()) {
+            if (deviceInfoBuilder_.isEmpty()) {
+              deviceInfoBuilder_.dispose();
+              deviceInfoBuilder_ = null;
+              deviceInfo_ = other.deviceInfo_;
+              bitField0_ = (bitField0_ & ~0x00000008);
+              deviceInfoBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getDeviceInfoFieldBuilder() : null;
+            } else {
+              deviceInfoBuilder_.addAllMessages(other.deviceInfo_);
+            }
+          }
+        }
+        if (other.hasBuildInfo()) {
+          mergeBuildInfo(other.getBuildInfo());
+        }
+        if (!other.subscriberEmail_.isEmpty()) {
+          if (subscriberEmail_.isEmpty()) {
+            subscriberEmail_ = other.subscriberEmail_;
+            bitField0_ = (bitField0_ & ~0x00000020);
+          } else {
+            ensureSubscriberEmailIsMutable();
+            subscriberEmail_.addAll(other.subscriberEmail_);
+          }
+          onChanged();
+        }
+        if (other.hasHostInfo()) {
+          mergeHostInfo(other.getHostInfo());
+        }
+        if (testCaseBuilder_ == null) {
+          if (!other.testCase_.isEmpty()) {
+            if (testCase_.isEmpty()) {
+              testCase_ = other.testCase_;
+              bitField0_ = (bitField0_ & ~0x00000080);
+            } else {
+              ensureTestCaseIsMutable();
+              testCase_.addAll(other.testCase_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.testCase_.isEmpty()) {
+            if (testCaseBuilder_.isEmpty()) {
+              testCaseBuilder_.dispose();
+              testCaseBuilder_ = null;
+              testCase_ = other.testCase_;
+              bitField0_ = (bitField0_ & ~0x00000080);
+              testCaseBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getTestCaseFieldBuilder() : null;
+            } else {
+              testCaseBuilder_.addAllMessages(other.testCase_);
+            }
+          }
+        }
+        if (profilingBuilder_ == null) {
+          if (!other.profiling_.isEmpty()) {
+            if (profiling_.isEmpty()) {
+              profiling_ = other.profiling_;
+              bitField0_ = (bitField0_ & ~0x00000100);
+            } else {
+              ensureProfilingIsMutable();
+              profiling_.addAll(other.profiling_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.profiling_.isEmpty()) {
+            if (profilingBuilder_.isEmpty()) {
+              profilingBuilder_.dispose();
+              profilingBuilder_ = null;
+              profiling_ = other.profiling_;
+              bitField0_ = (bitField0_ & ~0x00000100);
+              profilingBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getProfilingFieldBuilder() : null;
+            } else {
+              profilingBuilder_.addAllMessages(other.profiling_);
+            }
+          }
+        }
+        if (systraceBuilder_ == null) {
+          if (!other.systrace_.isEmpty()) {
+            if (systrace_.isEmpty()) {
+              systrace_ = other.systrace_;
+              bitField0_ = (bitField0_ & ~0x00000200);
+            } else {
+              ensureSystraceIsMutable();
+              systrace_.addAll(other.systrace_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.systrace_.isEmpty()) {
+            if (systraceBuilder_.isEmpty()) {
+              systraceBuilder_.dispose();
+              systraceBuilder_ = null;
+              systrace_ = other.systrace_;
+              bitField0_ = (bitField0_ & ~0x00000200);
+              systraceBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getSystraceFieldBuilder() : null;
+            } else {
+              systraceBuilder_.addAllMessages(other.systrace_);
+            }
+          }
+        }
+        if (other.hasStartTimestamp()) {
+          setStartTimestamp(other.getStartTimestamp());
+        }
+        if (other.hasEndTimestamp()) {
+          setEndTimestamp(other.getEndTimestamp());
+        }
+        if (coverageBuilder_ == null) {
+          if (!other.coverage_.isEmpty()) {
+            if (coverage_.isEmpty()) {
+              coverage_ = other.coverage_;
+              bitField0_ = (bitField0_ & ~0x00001000);
+            } else {
+              ensureCoverageIsMutable();
+              coverage_.addAll(other.coverage_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.coverage_.isEmpty()) {
+            if (coverageBuilder_.isEmpty()) {
+              coverageBuilder_.dispose();
+              coverageBuilder_ = null;
+              coverage_ = other.coverage_;
+              bitField0_ = (bitField0_ & ~0x00001000);
+              coverageBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getCoverageFieldBuilder() : null;
+            } else {
+              coverageBuilder_.addAllMessages(other.coverage_);
+            }
+          }
+        }
+        if (logBuilder_ == null) {
+          if (!other.log_.isEmpty()) {
+            if (log_.isEmpty()) {
+              log_ = other.log_;
+              bitField0_ = (bitField0_ & ~0x00002000);
+            } else {
+              ensureLogIsMutable();
+              log_.addAll(other.log_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.log_.isEmpty()) {
+            if (logBuilder_.isEmpty()) {
+              logBuilder_.dispose();
+              logBuilder_ = null;
+              log_ = other.log_;
+              bitField0_ = (bitField0_ & ~0x00002000);
+              logBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getLogFieldBuilder() : null;
+            } else {
+              logBuilder_.addAllMessages(other.log_);
+            }
+          }
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.TestReportMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.TestReportMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional bytes test_suite = 1 [deprecated = true];
+      private com.google.protobuf.ByteString testSuite_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * The test suite name..
+       * </pre>
+       */
+      @java.lang.Deprecated public boolean hasTestSuite() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * The test suite name..
+       * </pre>
+       */
+      @java.lang.Deprecated public com.google.protobuf.ByteString getTestSuite() {
+        return testSuite_;
+      }
+      /**
+       * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * The test suite name..
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder setTestSuite(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        testSuite_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes test_suite = 1 [deprecated = true];</code>
+       *
+       * <pre>
+       * The test suite name..
+       * </pre>
+       */
+      @java.lang.Deprecated public Builder clearTestSuite() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        testSuite_ = getDefaultInstance().getTestSuite();
+        onChanged();
+        return this;
+      }
+
+      // optional bytes test = 2;
+      private com.google.protobuf.ByteString test_ = com.google.protobuf.ByteString.EMPTY;
+      /**
+       * <code>optional bytes test = 2;</code>
+       *
+       * <pre>
+       * The test name.
+       * </pre>
+       */
+      public boolean hasTest() {
+        return ((bitField0_ & 0x00000002) == 0x00000002);
+      }
+      /**
+       * <code>optional bytes test = 2;</code>
+       *
+       * <pre>
+       * The test name.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getTest() {
+        return test_;
+      }
+      /**
+       * <code>optional bytes test = 2;</code>
+       *
+       * <pre>
+       * The test name.
+       * </pre>
+       */
+      public Builder setTest(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000002;
+        test_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional bytes test = 2;</code>
+       *
+       * <pre>
+       * The test name.
+       * </pre>
+       */
+      public Builder clearTest() {
+        bitField0_ = (bitField0_ & ~0x00000002);
+        test_ = getDefaultInstance().getTest();
+        onChanged();
+        return this;
+      }
+
+      // optional .android.vts.VtsTestType test_type = 3;
+      private com.android.vts.proto.VtsReportMessage.VtsTestType testType_ = com.android.vts.proto.VtsReportMessage.VtsTestType.UNKNOWN_VTS_TESTTYPE;
+      /**
+       * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+       *
+       * <pre>
+       * The test type
+       * </pre>
+       */
+      public boolean hasTestType() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+       *
+       * <pre>
+       * The test type
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.VtsTestType getTestType() {
+        return testType_;
+      }
+      /**
+       * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+       *
+       * <pre>
+       * The test type
+       * </pre>
+       */
+      public Builder setTestType(com.android.vts.proto.VtsReportMessage.VtsTestType value) {
+        if (value == null) {
+          throw new NullPointerException();
+        }
+        bitField0_ |= 0x00000004;
+        testType_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsTestType test_type = 3;</code>
+       *
+       * <pre>
+       * The test type
+       * </pre>
+       */
+      public Builder clearTestType() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        testType_ = com.android.vts.proto.VtsReportMessage.VtsTestType.UNKNOWN_VTS_TESTTYPE;
+        onChanged();
+        return this;
+      }
+
+      // repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage> deviceInfo_ =
+        java.util.Collections.emptyList();
+      private void ensureDeviceInfoIsMutable() {
+        if (!((bitField0_ & 0x00000008) == 0x00000008)) {
+          deviceInfo_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage>(deviceInfo_);
+          bitField0_ |= 0x00000008;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder> deviceInfoBuilder_;
+
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage> getDeviceInfoList() {
+        if (deviceInfoBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(deviceInfo_);
+        } else {
+          return deviceInfoBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public int getDeviceInfoCount() {
+        if (deviceInfoBuilder_ == null) {
+          return deviceInfo_.size();
+        } else {
+          return deviceInfoBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage getDeviceInfo(int index) {
+        if (deviceInfoBuilder_ == null) {
+          return deviceInfo_.get(index);
+        } else {
+          return deviceInfoBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder setDeviceInfo(
+          int index, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage value) {
+        if (deviceInfoBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.set(index, value);
+          onChanged();
+        } else {
+          deviceInfoBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder setDeviceInfo(
+          int index, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder builderForValue) {
+        if (deviceInfoBuilder_ == null) {
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          deviceInfoBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder addDeviceInfo(com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage value) {
+        if (deviceInfoBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.add(value);
+          onChanged();
+        } else {
+          deviceInfoBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder addDeviceInfo(
+          int index, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage value) {
+        if (deviceInfoBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.add(index, value);
+          onChanged();
+        } else {
+          deviceInfoBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder addDeviceInfo(
+          com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder builderForValue) {
+        if (deviceInfoBuilder_ == null) {
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.add(builderForValue.build());
+          onChanged();
+        } else {
+          deviceInfoBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder addDeviceInfo(
+          int index, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder builderForValue) {
+        if (deviceInfoBuilder_ == null) {
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          deviceInfoBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder addAllDeviceInfo(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage> values) {
+        if (deviceInfoBuilder_ == null) {
+          ensureDeviceInfoIsMutable();
+          super.addAll(values, deviceInfo_);
+          onChanged();
+        } else {
+          deviceInfoBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder clearDeviceInfo() {
+        if (deviceInfoBuilder_ == null) {
+          deviceInfo_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000008);
+          onChanged();
+        } else {
+          deviceInfoBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public Builder removeDeviceInfo(int index) {
+        if (deviceInfoBuilder_ == null) {
+          ensureDeviceInfoIsMutable();
+          deviceInfo_.remove(index);
+          onChanged();
+        } else {
+          deviceInfoBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder getDeviceInfoBuilder(
+          int index) {
+        return getDeviceInfoFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder getDeviceInfoOrBuilder(
+          int index) {
+        if (deviceInfoBuilder_ == null) {
+          return deviceInfo_.get(index);  } else {
+          return deviceInfoBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder> 
+           getDeviceInfoOrBuilderList() {
+        if (deviceInfoBuilder_ != null) {
+          return deviceInfoBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(deviceInfo_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder addDeviceInfoBuilder() {
+        return getDeviceInfoFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder addDeviceInfoBuilder(
+          int index) {
+        return getDeviceInfoFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.AndroidDeviceInfoMessage device_info = 4;</code>
+       *
+       * <pre>
+       * Target device info
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder> 
+           getDeviceInfoBuilderList() {
+        return getDeviceInfoFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder> 
+          getDeviceInfoFieldBuilder() {
+        if (deviceInfoBuilder_ == null) {
+          deviceInfoBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage.Builder, com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessageOrBuilder>(
+                  deviceInfo_,
+                  ((bitField0_ & 0x00000008) == 0x00000008),
+                  getParentForChildren(),
+                  isClean());
+          deviceInfo_ = null;
+        }
+        return deviceInfoBuilder_;
+      }
+
+      // optional .android.vts.AndroidBuildInfo build_info = 5;
+      private com.android.vts.proto.VtsReportMessage.AndroidBuildInfo buildInfo_ = com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance();
+      private com.google.protobuf.SingleFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.AndroidBuildInfo, com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder, com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder> buildInfoBuilder_;
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public boolean hasBuildInfo() {
+        return ((bitField0_ & 0x00000010) == 0x00000010);
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidBuildInfo getBuildInfo() {
+        if (buildInfoBuilder_ == null) {
+          return buildInfo_;
+        } else {
+          return buildInfoBuilder_.getMessage();
+        }
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public Builder setBuildInfo(com.android.vts.proto.VtsReportMessage.AndroidBuildInfo value) {
+        if (buildInfoBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          buildInfo_ = value;
+          onChanged();
+        } else {
+          buildInfoBuilder_.setMessage(value);
+        }
+        bitField0_ |= 0x00000010;
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public Builder setBuildInfo(
+          com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder builderForValue) {
+        if (buildInfoBuilder_ == null) {
+          buildInfo_ = builderForValue.build();
+          onChanged();
+        } else {
+          buildInfoBuilder_.setMessage(builderForValue.build());
+        }
+        bitField0_ |= 0x00000010;
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public Builder mergeBuildInfo(com.android.vts.proto.VtsReportMessage.AndroidBuildInfo value) {
+        if (buildInfoBuilder_ == null) {
+          if (((bitField0_ & 0x00000010) == 0x00000010) &&
+              buildInfo_ != com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance()) {
+            buildInfo_ =
+              com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.newBuilder(buildInfo_).mergeFrom(value).buildPartial();
+          } else {
+            buildInfo_ = value;
+          }
+          onChanged();
+        } else {
+          buildInfoBuilder_.mergeFrom(value);
+        }
+        bitField0_ |= 0x00000010;
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public Builder clearBuildInfo() {
+        if (buildInfoBuilder_ == null) {
+          buildInfo_ = com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.getDefaultInstance();
+          onChanged();
+        } else {
+          buildInfoBuilder_.clear();
+        }
+        bitField0_ = (bitField0_ & ~0x00000010);
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder getBuildInfoBuilder() {
+        bitField0_ |= 0x00000010;
+        onChanged();
+        return getBuildInfoFieldBuilder().getBuilder();
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder getBuildInfoOrBuilder() {
+        if (buildInfoBuilder_ != null) {
+          return buildInfoBuilder_.getMessageOrBuilder();
+        } else {
+          return buildInfo_;
+        }
+      }
+      /**
+       * <code>optional .android.vts.AndroidBuildInfo build_info = 5;</code>
+       *
+       * <pre>
+       * Build info
+       * </pre>
+       */
+      private com.google.protobuf.SingleFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.AndroidBuildInfo, com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder, com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder> 
+          getBuildInfoFieldBuilder() {
+        if (buildInfoBuilder_ == null) {
+          buildInfoBuilder_ = new com.google.protobuf.SingleFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.AndroidBuildInfo, com.android.vts.proto.VtsReportMessage.AndroidBuildInfo.Builder, com.android.vts.proto.VtsReportMessage.AndroidBuildInfoOrBuilder>(
+                  buildInfo_,
+                  getParentForChildren(),
+                  isClean());
+          buildInfo_ = null;
+        }
+        return buildInfoBuilder_;
+      }
+
+      // repeated bytes subscriber_email = 6;
+      private java.util.List<com.google.protobuf.ByteString> subscriberEmail_ = java.util.Collections.emptyList();
+      private void ensureSubscriberEmailIsMutable() {
+        if (!((bitField0_ & 0x00000020) == 0x00000020)) {
+          subscriberEmail_ = new java.util.ArrayList<com.google.protobuf.ByteString>(subscriberEmail_);
+          bitField0_ |= 0x00000020;
+         }
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public java.util.List<com.google.protobuf.ByteString>
+          getSubscriberEmailList() {
+        return java.util.Collections.unmodifiableList(subscriberEmail_);
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public int getSubscriberEmailCount() {
+        return subscriberEmail_.size();
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public com.google.protobuf.ByteString getSubscriberEmail(int index) {
+        return subscriberEmail_.get(index);
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public Builder setSubscriberEmail(
+          int index, com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureSubscriberEmailIsMutable();
+        subscriberEmail_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public Builder addSubscriberEmail(com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureSubscriberEmailIsMutable();
+        subscriberEmail_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public Builder addAllSubscriberEmail(
+          java.lang.Iterable<? extends com.google.protobuf.ByteString> values) {
+        ensureSubscriberEmailIsMutable();
+        super.addAll(values, subscriberEmail_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated bytes subscriber_email = 6;</code>
+       *
+       * <pre>
+       * Email addresses of subscribers to the test results
+       * </pre>
+       */
+      public Builder clearSubscriberEmail() {
+        subscriberEmail_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000020);
+        onChanged();
+        return this;
+      }
+
+      // optional .android.vts.VtsHostInfo host_info = 7;
+      private com.android.vts.proto.VtsReportMessage.VtsHostInfo hostInfo_ = com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance();
+      private com.google.protobuf.SingleFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.VtsHostInfo, com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder, com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder> hostInfoBuilder_;
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public boolean hasHostInfo() {
+        return ((bitField0_ & 0x00000040) == 0x00000040);
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.VtsHostInfo getHostInfo() {
+        if (hostInfoBuilder_ == null) {
+          return hostInfo_;
+        } else {
+          return hostInfoBuilder_.getMessage();
+        }
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public Builder setHostInfo(com.android.vts.proto.VtsReportMessage.VtsHostInfo value) {
+        if (hostInfoBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          hostInfo_ = value;
+          onChanged();
+        } else {
+          hostInfoBuilder_.setMessage(value);
+        }
+        bitField0_ |= 0x00000040;
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public Builder setHostInfo(
+          com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder builderForValue) {
+        if (hostInfoBuilder_ == null) {
+          hostInfo_ = builderForValue.build();
+          onChanged();
+        } else {
+          hostInfoBuilder_.setMessage(builderForValue.build());
+        }
+        bitField0_ |= 0x00000040;
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public Builder mergeHostInfo(com.android.vts.proto.VtsReportMessage.VtsHostInfo value) {
+        if (hostInfoBuilder_ == null) {
+          if (((bitField0_ & 0x00000040) == 0x00000040) &&
+              hostInfo_ != com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance()) {
+            hostInfo_ =
+              com.android.vts.proto.VtsReportMessage.VtsHostInfo.newBuilder(hostInfo_).mergeFrom(value).buildPartial();
+          } else {
+            hostInfo_ = value;
+          }
+          onChanged();
+        } else {
+          hostInfoBuilder_.mergeFrom(value);
+        }
+        bitField0_ |= 0x00000040;
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public Builder clearHostInfo() {
+        if (hostInfoBuilder_ == null) {
+          hostInfo_ = com.android.vts.proto.VtsReportMessage.VtsHostInfo.getDefaultInstance();
+          onChanged();
+        } else {
+          hostInfoBuilder_.clear();
+        }
+        bitField0_ = (bitField0_ & ~0x00000040);
+        return this;
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder getHostInfoBuilder() {
+        bitField0_ |= 0x00000040;
+        onChanged();
+        return getHostInfoFieldBuilder().getBuilder();
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder getHostInfoOrBuilder() {
+        if (hostInfoBuilder_ != null) {
+          return hostInfoBuilder_.getMessageOrBuilder();
+        } else {
+          return hostInfo_;
+        }
+      }
+      /**
+       * <code>optional .android.vts.VtsHostInfo host_info = 7;</code>
+       *
+       * <pre>
+       * Info about the host computer
+       * </pre>
+       */
+      private com.google.protobuf.SingleFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.VtsHostInfo, com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder, com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder> 
+          getHostInfoFieldBuilder() {
+        if (hostInfoBuilder_ == null) {
+          hostInfoBuilder_ = new com.google.protobuf.SingleFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.VtsHostInfo, com.android.vts.proto.VtsReportMessage.VtsHostInfo.Builder, com.android.vts.proto.VtsReportMessage.VtsHostInfoOrBuilder>(
+                  hostInfo_,
+                  getParentForChildren(),
+                  isClean());
+          hostInfo_ = null;
+        }
+        return hostInfoBuilder_;
+      }
+
+      // repeated .android.vts.TestCaseReportMessage test_case = 11;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage> testCase_ =
+        java.util.Collections.emptyList();
+      private void ensureTestCaseIsMutable() {
+        if (!((bitField0_ & 0x00000080) == 0x00000080)) {
+          testCase_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage>(testCase_);
+          bitField0_ |= 0x00000080;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.TestCaseReportMessage, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder> testCaseBuilder_;
+
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage> getTestCaseList() {
+        if (testCaseBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(testCase_);
+        } else {
+          return testCaseBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public int getTestCaseCount() {
+        if (testCaseBuilder_ == null) {
+          return testCase_.size();
+        } else {
+          return testCaseBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage getTestCase(int index) {
+        if (testCaseBuilder_ == null) {
+          return testCase_.get(index);
+        } else {
+          return testCaseBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder setTestCase(
+          int index, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage value) {
+        if (testCaseBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestCaseIsMutable();
+          testCase_.set(index, value);
+          onChanged();
+        } else {
+          testCaseBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder setTestCase(
+          int index, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder builderForValue) {
+        if (testCaseBuilder_ == null) {
+          ensureTestCaseIsMutable();
+          testCase_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          testCaseBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder addTestCase(com.android.vts.proto.VtsReportMessage.TestCaseReportMessage value) {
+        if (testCaseBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestCaseIsMutable();
+          testCase_.add(value);
+          onChanged();
+        } else {
+          testCaseBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder addTestCase(
+          int index, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage value) {
+        if (testCaseBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestCaseIsMutable();
+          testCase_.add(index, value);
+          onChanged();
+        } else {
+          testCaseBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder addTestCase(
+          com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder builderForValue) {
+        if (testCaseBuilder_ == null) {
+          ensureTestCaseIsMutable();
+          testCase_.add(builderForValue.build());
+          onChanged();
+        } else {
+          testCaseBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder addTestCase(
+          int index, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder builderForValue) {
+        if (testCaseBuilder_ == null) {
+          ensureTestCaseIsMutable();
+          testCase_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          testCaseBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder addAllTestCase(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.TestCaseReportMessage> values) {
+        if (testCaseBuilder_ == null) {
+          ensureTestCaseIsMutable();
+          super.addAll(values, testCase_);
+          onChanged();
+        } else {
+          testCaseBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder clearTestCase() {
+        if (testCaseBuilder_ == null) {
+          testCase_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000080);
+          onChanged();
+        } else {
+          testCaseBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public Builder removeTestCase(int index) {
+        if (testCaseBuilder_ == null) {
+          ensureTestCaseIsMutable();
+          testCase_.remove(index);
+          onChanged();
+        } else {
+          testCaseBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder getTestCaseBuilder(
+          int index) {
+        return getTestCaseFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder getTestCaseOrBuilder(
+          int index) {
+        if (testCaseBuilder_ == null) {
+          return testCase_.get(index);  } else {
+          return testCaseBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder> 
+           getTestCaseOrBuilderList() {
+        if (testCaseBuilder_ != null) {
+          return testCaseBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(testCase_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder addTestCaseBuilder() {
+        return getTestCaseFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder addTestCaseBuilder(
+          int index) {
+        return getTestCaseFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.TestCaseReportMessage test_case = 11;</code>
+       *
+       * <pre>
+       * Test case reports
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder> 
+           getTestCaseBuilderList() {
+        return getTestCaseFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.TestCaseReportMessage, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder> 
+          getTestCaseFieldBuilder() {
+        if (testCaseBuilder_ == null) {
+          testCaseBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.TestCaseReportMessage, com.android.vts.proto.VtsReportMessage.TestCaseReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestCaseReportMessageOrBuilder>(
+                  testCase_,
+                  ((bitField0_ & 0x00000080) == 0x00000080),
+                  getParentForChildren(),
+                  isClean());
+          testCase_ = null;
+        }
+        return testCaseBuilder_;
+      }
+
+      // repeated .android.vts.ProfilingReportMessage profiling = 21;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> profiling_ =
+        java.util.Collections.emptyList();
+      private void ensureProfilingIsMutable() {
+        if (!((bitField0_ & 0x00000100) == 0x00000100)) {
+          profiling_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage>(profiling_);
+          bitField0_ |= 0x00000100;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.ProfilingReportMessage, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder, com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> profilingBuilder_;
+
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> getProfilingList() {
+        if (profilingBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(profiling_);
+        } else {
+          return profilingBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public int getProfilingCount() {
+        if (profilingBuilder_ == null) {
+          return profiling_.size();
+        } else {
+          return profilingBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage getProfiling(int index) {
+        if (profilingBuilder_ == null) {
+          return profiling_.get(index);
+        } else {
+          return profilingBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder setProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage value) {
+        if (profilingBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureProfilingIsMutable();
+          profiling_.set(index, value);
+          onChanged();
+        } else {
+          profilingBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder setProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder builderForValue) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          profilingBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(com.android.vts.proto.VtsReportMessage.ProfilingReportMessage value) {
+        if (profilingBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureProfilingIsMutable();
+          profiling_.add(value);
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage value) {
+        if (profilingBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureProfilingIsMutable();
+          profiling_.add(index, value);
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(
+          com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder builderForValue) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.add(builderForValue.build());
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder addProfiling(
+          int index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder builderForValue) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          profilingBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder addAllProfiling(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessage> values) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          super.addAll(values, profiling_);
+          onChanged();
+        } else {
+          profilingBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder clearProfiling() {
+        if (profilingBuilder_ == null) {
+          profiling_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000100);
+          onChanged();
+        } else {
+          profilingBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public Builder removeProfiling(int index) {
+        if (profilingBuilder_ == null) {
+          ensureProfilingIsMutable();
+          profiling_.remove(index);
+          onChanged();
+        } else {
+          profilingBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder getProfilingBuilder(
+          int index) {
+        return getProfilingFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder getProfilingOrBuilder(
+          int index) {
+        if (profilingBuilder_ == null) {
+          return profiling_.get(index);  } else {
+          return profilingBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+           getProfilingOrBuilderList() {
+        if (profilingBuilder_ != null) {
+          return profilingBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(profiling_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder addProfilingBuilder() {
+        return getProfilingFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder addProfilingBuilder(
+          int index) {
+        return getProfilingFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.ProfilingReportMessage profiling = 21;</code>
+       *
+       * <pre>
+       * Profiling reports
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder> 
+           getProfilingBuilderList() {
+        return getProfilingFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.ProfilingReportMessage, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder, com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder> 
+          getProfilingFieldBuilder() {
+        if (profilingBuilder_ == null) {
+          profilingBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.ProfilingReportMessage, com.android.vts.proto.VtsReportMessage.ProfilingReportMessage.Builder, com.android.vts.proto.VtsReportMessage.ProfilingReportMessageOrBuilder>(
+                  profiling_,
+                  ((bitField0_ & 0x00000100) == 0x00000100),
+                  getParentForChildren(),
+                  isClean());
+          profiling_ = null;
+        }
+        return profilingBuilder_;
+      }
+
+      // repeated .android.vts.SystraceReportMessage systrace = 22;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> systrace_ =
+        java.util.Collections.emptyList();
+      private void ensureSystraceIsMutable() {
+        if (!((bitField0_ & 0x00000200) == 0x00000200)) {
+          systrace_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.SystraceReportMessage>(systrace_);
+          bitField0_ |= 0x00000200;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.SystraceReportMessage, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder, com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> systraceBuilder_;
+
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage> getSystraceList() {
+        if (systraceBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(systrace_);
+        } else {
+          return systraceBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public int getSystraceCount() {
+        if (systraceBuilder_ == null) {
+          return systrace_.size();
+        } else {
+          return systraceBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage getSystrace(int index) {
+        if (systraceBuilder_ == null) {
+          return systrace_.get(index);
+        } else {
+          return systraceBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder setSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage value) {
+        if (systraceBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureSystraceIsMutable();
+          systrace_.set(index, value);
+          onChanged();
+        } else {
+          systraceBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder setSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder builderForValue) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          systraceBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder addSystrace(com.android.vts.proto.VtsReportMessage.SystraceReportMessage value) {
+        if (systraceBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureSystraceIsMutable();
+          systrace_.add(value);
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder addSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage value) {
+        if (systraceBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureSystraceIsMutable();
+          systrace_.add(index, value);
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder addSystrace(
+          com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder builderForValue) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.add(builderForValue.build());
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder addSystrace(
+          int index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder builderForValue) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          systraceBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder addAllSystrace(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessage> values) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          super.addAll(values, systrace_);
+          onChanged();
+        } else {
+          systraceBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder clearSystrace() {
+        if (systraceBuilder_ == null) {
+          systrace_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000200);
+          onChanged();
+        } else {
+          systraceBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public Builder removeSystrace(int index) {
+        if (systraceBuilder_ == null) {
+          ensureSystraceIsMutable();
+          systrace_.remove(index);
+          onChanged();
+        } else {
+          systraceBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder getSystraceBuilder(
+          int index) {
+        return getSystraceFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder getSystraceOrBuilder(
+          int index) {
+        if (systraceBuilder_ == null) {
+          return systrace_.get(index);  } else {
+          return systraceBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+           getSystraceOrBuilderList() {
+        if (systraceBuilder_ != null) {
+          return systraceBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(systrace_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder addSystraceBuilder() {
+        return getSystraceFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.SystraceReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder addSystraceBuilder(
+          int index) {
+        return getSystraceFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.SystraceReportMessage systrace = 22;</code>
+       *
+       * <pre>
+       * Systrace report per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder> 
+           getSystraceBuilderList() {
+        return getSystraceFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.SystraceReportMessage, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder, com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder> 
+          getSystraceFieldBuilder() {
+        if (systraceBuilder_ == null) {
+          systraceBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.SystraceReportMessage, com.android.vts.proto.VtsReportMessage.SystraceReportMessage.Builder, com.android.vts.proto.VtsReportMessage.SystraceReportMessageOrBuilder>(
+                  systrace_,
+                  ((bitField0_ & 0x00000200) == 0x00000200),
+                  getParentForChildren(),
+                  isClean());
+          systrace_ = null;
+        }
+        return systraceBuilder_;
+      }
+
+      // optional int64 start_timestamp = 101;
+      private long startTimestamp_ ;
+      /**
+       * <code>optional int64 start_timestamp = 101;</code>
+       *
+       * <pre>
+       * Execution start and end time stamp.
+       * </pre>
+       */
+      public boolean hasStartTimestamp() {
+        return ((bitField0_ & 0x00000400) == 0x00000400);
+      }
+      /**
+       * <code>optional int64 start_timestamp = 101;</code>
+       *
+       * <pre>
+       * Execution start and end time stamp.
+       * </pre>
+       */
+      public long getStartTimestamp() {
+        return startTimestamp_;
+      }
+      /**
+       * <code>optional int64 start_timestamp = 101;</code>
+       *
+       * <pre>
+       * Execution start and end time stamp.
+       * </pre>
+       */
+      public Builder setStartTimestamp(long value) {
+        bitField0_ |= 0x00000400;
+        startTimestamp_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int64 start_timestamp = 101;</code>
+       *
+       * <pre>
+       * Execution start and end time stamp.
+       * </pre>
+       */
+      public Builder clearStartTimestamp() {
+        bitField0_ = (bitField0_ & ~0x00000400);
+        startTimestamp_ = 0L;
+        onChanged();
+        return this;
+      }
+
+      // optional int64 end_timestamp = 102;
+      private long endTimestamp_ ;
+      /**
+       * <code>optional int64 end_timestamp = 102;</code>
+       */
+      public boolean hasEndTimestamp() {
+        return ((bitField0_ & 0x00000800) == 0x00000800);
+      }
+      /**
+       * <code>optional int64 end_timestamp = 102;</code>
+       */
+      public long getEndTimestamp() {
+        return endTimestamp_;
+      }
+      /**
+       * <code>optional int64 end_timestamp = 102;</code>
+       */
+      public Builder setEndTimestamp(long value) {
+        bitField0_ |= 0x00000800;
+        endTimestamp_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional int64 end_timestamp = 102;</code>
+       */
+      public Builder clearEndTimestamp() {
+        bitField0_ = (bitField0_ & ~0x00000800);
+        endTimestamp_ = 0L;
+        onChanged();
+        return this;
+      }
+
+      // repeated .android.vts.CoverageReportMessage coverage = 103;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> coverage_ =
+        java.util.Collections.emptyList();
+      private void ensureCoverageIsMutable() {
+        if (!((bitField0_ & 0x00001000) == 0x00001000)) {
+          coverage_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.CoverageReportMessage>(coverage_);
+          bitField0_ |= 0x00001000;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.CoverageReportMessage, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder, com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> coverageBuilder_;
+
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage> getCoverageList() {
+        if (coverageBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(coverage_);
+        } else {
+          return coverageBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public int getCoverageCount() {
+        if (coverageBuilder_ == null) {
+          return coverage_.size();
+        } else {
+          return coverageBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage getCoverage(int index) {
+        if (coverageBuilder_ == null) {
+          return coverage_.get(index);
+        } else {
+          return coverageBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder setCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage value) {
+        if (coverageBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureCoverageIsMutable();
+          coverage_.set(index, value);
+          onChanged();
+        } else {
+          coverageBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder setCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder builderForValue) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          coverageBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(com.android.vts.proto.VtsReportMessage.CoverageReportMessage value) {
+        if (coverageBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureCoverageIsMutable();
+          coverage_.add(value);
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage value) {
+        if (coverageBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureCoverageIsMutable();
+          coverage_.add(index, value);
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(
+          com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder builderForValue) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.add(builderForValue.build());
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder addCoverage(
+          int index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder builderForValue) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          coverageBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder addAllCoverage(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessage> values) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          super.addAll(values, coverage_);
+          onChanged();
+        } else {
+          coverageBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder clearCoverage() {
+        if (coverageBuilder_ == null) {
+          coverage_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00001000);
+          onChanged();
+        } else {
+          coverageBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public Builder removeCoverage(int index) {
+        if (coverageBuilder_ == null) {
+          ensureCoverageIsMutable();
+          coverage_.remove(index);
+          onChanged();
+        } else {
+          coverageBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder getCoverageBuilder(
+          int index) {
+        return getCoverageFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder getCoverageOrBuilder(
+          int index) {
+        if (coverageBuilder_ == null) {
+          return coverage_.get(index);  } else {
+          return coverageBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+           getCoverageOrBuilderList() {
+        if (coverageBuilder_ != null) {
+          return coverageBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(coverage_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder addCoverageBuilder() {
+        return getCoverageFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.CoverageReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder addCoverageBuilder(
+          int index) {
+        return getCoverageFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.CoverageReportMessage coverage = 103;</code>
+       *
+       * <pre>
+       * Coverage report per file
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder> 
+           getCoverageBuilderList() {
+        return getCoverageFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.CoverageReportMessage, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder, com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder> 
+          getCoverageFieldBuilder() {
+        if (coverageBuilder_ == null) {
+          coverageBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.CoverageReportMessage, com.android.vts.proto.VtsReportMessage.CoverageReportMessage.Builder, com.android.vts.proto.VtsReportMessage.CoverageReportMessageOrBuilder>(
+                  coverage_,
+                  ((bitField0_ & 0x00001000) == 0x00001000),
+                  getParentForChildren(),
+                  isClean());
+          coverage_ = null;
+        }
+        return coverageBuilder_;
+      }
+
+      // repeated .android.vts.LogMessage log = 1001;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> log_ =
+        java.util.Collections.emptyList();
+      private void ensureLogIsMutable() {
+        if (!((bitField0_ & 0x00002000) == 0x00002000)) {
+          log_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.LogMessage>(log_);
+          bitField0_ |= 0x00002000;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.LogMessage, com.android.vts.proto.VtsReportMessage.LogMessage.Builder, com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> logBuilder_;
+
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage> getLogList() {
+        if (logBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(log_);
+        } else {
+          return logBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public int getLogCount() {
+        if (logBuilder_ == null) {
+          return log_.size();
+        } else {
+          return logBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage getLog(int index) {
+        if (logBuilder_ == null) {
+          return log_.get(index);
+        } else {
+          return logBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder setLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage value) {
+        if (logBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureLogIsMutable();
+          log_.set(index, value);
+          onChanged();
+        } else {
+          logBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder setLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage.Builder builderForValue) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          logBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(com.android.vts.proto.VtsReportMessage.LogMessage value) {
+        if (logBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureLogIsMutable();
+          log_.add(value);
+          onChanged();
+        } else {
+          logBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage value) {
+        if (logBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureLogIsMutable();
+          log_.add(index, value);
+          onChanged();
+        } else {
+          logBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(
+          com.android.vts.proto.VtsReportMessage.LogMessage.Builder builderForValue) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.add(builderForValue.build());
+          onChanged();
+        } else {
+          logBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addLog(
+          int index, com.android.vts.proto.VtsReportMessage.LogMessage.Builder builderForValue) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          logBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder addAllLog(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.LogMessage> values) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          super.addAll(values, log_);
+          onChanged();
+        } else {
+          logBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder clearLog() {
+        if (logBuilder_ == null) {
+          log_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00002000);
+          onChanged();
+        } else {
+          logBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public Builder removeLog(int index) {
+        if (logBuilder_ == null) {
+          ensureLogIsMutable();
+          log_.remove(index);
+          onChanged();
+        } else {
+          logBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage.Builder getLogBuilder(
+          int index) {
+        return getLogFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder getLogOrBuilder(
+          int index) {
+        if (logBuilder_ == null) {
+          return log_.get(index);  } else {
+          return logBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+           getLogOrBuilderList() {
+        if (logBuilder_ != null) {
+          return logBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(log_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage.Builder addLogBuilder() {
+        return getLogFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.LogMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.LogMessage.Builder addLogBuilder(
+          int index) {
+        return getLogFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.LogMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.LogMessage log = 1001;</code>
+       *
+       * <pre>
+       * Log for a test module. May contain multiple logs such as logcat, host log,
+       * etc.
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.LogMessage.Builder> 
+           getLogBuilderList() {
+        return getLogFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.LogMessage, com.android.vts.proto.VtsReportMessage.LogMessage.Builder, com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder> 
+          getLogFieldBuilder() {
+        if (logBuilder_ == null) {
+          logBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.LogMessage, com.android.vts.proto.VtsReportMessage.LogMessage.Builder, com.android.vts.proto.VtsReportMessage.LogMessageOrBuilder>(
+                  log_,
+                  ((bitField0_ & 0x00002000) == 0x00002000),
+                  getParentForChildren(),
+                  isClean());
+          log_ = null;
+        }
+        return logBuilder_;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.TestReportMessage)
+    }
+
+    static {
+      defaultInstance = new TestReportMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.TestReportMessage)
+  }
+
+  public interface TestPlanReportMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // repeated string test_module_name = 11;
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    java.util.List<java.lang.String>
+    getTestModuleNameList();
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    int getTestModuleNameCount();
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    java.lang.String getTestModuleName(int index);
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    com.google.protobuf.ByteString
+        getTestModuleNameBytes(int index);
+
+    // repeated int64 test_module_start_timestamp = 12;
+    /**
+     * <code>repeated int64 test_module_start_timestamp = 12;</code>
+     */
+    java.util.List<java.lang.Long> getTestModuleStartTimestampList();
+    /**
+     * <code>repeated int64 test_module_start_timestamp = 12;</code>
+     */
+    int getTestModuleStartTimestampCount();
+    /**
+     * <code>repeated int64 test_module_start_timestamp = 12;</code>
+     */
+    long getTestModuleStartTimestamp(int index);
+
+    // optional string test_plan_name = 21;
+    /**
+     * <code>optional string test_plan_name = 21;</code>
+     *
+     * <pre>
+     * The test plan name.
+     * </pre>
+     */
+    boolean hasTestPlanName();
+    /**
+     * <code>optional string test_plan_name = 21;</code>
+     *
+     * <pre>
+     * The test plan name.
+     * </pre>
+     */
+    java.lang.String getTestPlanName();
+    /**
+     * <code>optional string test_plan_name = 21;</code>
+     *
+     * <pre>
+     * The test plan name.
+     * </pre>
+     */
+    com.google.protobuf.ByteString
+        getTestPlanNameBytes();
+
+    // repeated .android.vts.UrlResourceMessage partner_report = 31;
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.UrlResourceMessage> 
+        getPartnerReportList();
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.UrlResourceMessage getPartnerReport(int index);
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    int getPartnerReportCount();
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder> 
+        getPartnerReportOrBuilderList();
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder getPartnerReportOrBuilder(
+        int index);
+  }
+  /**
+   * Protobuf type {@code android.vts.TestPlanReportMessage}
+   *
+   * <pre>
+   * To specify a test execution report.
+   * </pre>
+   */
+  public static final class TestPlanReportMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements TestPlanReportMessageOrBuilder {
+    // Use TestPlanReportMessage.newBuilder() to construct.
+    private TestPlanReportMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private TestPlanReportMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final TestPlanReportMessage defaultInstance;
+    public static TestPlanReportMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public TestPlanReportMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private TestPlanReportMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 90: {
+              if (!((mutable_bitField0_ & 0x00000001) == 0x00000001)) {
+                testModuleName_ = new com.google.protobuf.LazyStringArrayList();
+                mutable_bitField0_ |= 0x00000001;
+              }
+              testModuleName_.add(input.readBytes());
+              break;
+            }
+            case 96: {
+              if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
+                testModuleStartTimestamp_ = new java.util.ArrayList<java.lang.Long>();
+                mutable_bitField0_ |= 0x00000002;
+              }
+              testModuleStartTimestamp_.add(input.readInt64());
+              break;
+            }
+            case 98: {
+              int length = input.readRawVarint32();
+              int limit = input.pushLimit(length);
+              if (!((mutable_bitField0_ & 0x00000002) == 0x00000002) && input.getBytesUntilLimit() > 0) {
+                testModuleStartTimestamp_ = new java.util.ArrayList<java.lang.Long>();
+                mutable_bitField0_ |= 0x00000002;
+              }
+              while (input.getBytesUntilLimit() > 0) {
+                testModuleStartTimestamp_.add(input.readInt64());
+              }
+              input.popLimit(limit);
+              break;
+            }
+            case 170: {
+              bitField0_ |= 0x00000001;
+              testPlanName_ = input.readBytes();
+              break;
+            }
+            case 250: {
+              if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+                partnerReport_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.UrlResourceMessage>();
+                mutable_bitField0_ |= 0x00000008;
+              }
+              partnerReport_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.UrlResourceMessage.PARSER, extensionRegistry));
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000001) == 0x00000001)) {
+          testModuleName_ = new com.google.protobuf.UnmodifiableLazyStringList(testModuleName_);
+        }
+        if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
+          testModuleStartTimestamp_ = java.util.Collections.unmodifiableList(testModuleStartTimestamp_);
+        }
+        if (((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
+          partnerReport_ = java.util.Collections.unmodifiableList(partnerReport_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestPlanReportMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestPlanReportMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.class, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<TestPlanReportMessage> PARSER =
+        new com.google.protobuf.AbstractParser<TestPlanReportMessage>() {
+      public TestPlanReportMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new TestPlanReportMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<TestPlanReportMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // repeated string test_module_name = 11;
+    public static final int TEST_MODULE_NAME_FIELD_NUMBER = 11;
+    private com.google.protobuf.LazyStringList testModuleName_;
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    public java.util.List<java.lang.String>
+        getTestModuleNameList() {
+      return testModuleName_;
+    }
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    public int getTestModuleNameCount() {
+      return testModuleName_.size();
+    }
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    public java.lang.String getTestModuleName(int index) {
+      return testModuleName_.get(index);
+    }
+    /**
+     * <code>repeated string test_module_name = 11;</code>
+     *
+     * <pre>
+     * Keys used to find all TestReportMessage messages of test modules in
+     * this plan.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString
+        getTestModuleNameBytes(int index) {
+      return testModuleName_.getByteString(index);
+    }
+
+    // repeated int64 test_module_start_timestamp = 12;
+    public static final int TEST_MODULE_START_TIMESTAMP_FIELD_NUMBER = 12;
+    private java.util.List<java.lang.Long> testModuleStartTimestamp_;
+    /**
+     * <code>repeated int64 test_module_start_timestamp = 12;</code>
+     */
+    public java.util.List<java.lang.Long>
+        getTestModuleStartTimestampList() {
+      return testModuleStartTimestamp_;
+    }
+    /**
+     * <code>repeated int64 test_module_start_timestamp = 12;</code>
+     */
+    public int getTestModuleStartTimestampCount() {
+      return testModuleStartTimestamp_.size();
+    }
+    /**
+     * <code>repeated int64 test_module_start_timestamp = 12;</code>
+     */
+    public long getTestModuleStartTimestamp(int index) {
+      return testModuleStartTimestamp_.get(index);
+    }
+
+    // optional string test_plan_name = 21;
+    public static final int TEST_PLAN_NAME_FIELD_NUMBER = 21;
+    private java.lang.Object testPlanName_;
+    /**
+     * <code>optional string test_plan_name = 21;</code>
+     *
+     * <pre>
+     * The test plan name.
+     * </pre>
+     */
+    public boolean hasTestPlanName() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional string test_plan_name = 21;</code>
+     *
+     * <pre>
+     * The test plan name.
+     * </pre>
+     */
+    public java.lang.String getTestPlanName() {
+      java.lang.Object ref = testPlanName_;
+      if (ref instanceof java.lang.String) {
+        return (java.lang.String) ref;
+      } else {
+        com.google.protobuf.ByteString bs = 
+            (com.google.protobuf.ByteString) ref;
+        java.lang.String s = bs.toStringUtf8();
+        if (bs.isValidUtf8()) {
+          testPlanName_ = s;
+        }
+        return s;
+      }
+    }
+    /**
+     * <code>optional string test_plan_name = 21;</code>
+     *
+     * <pre>
+     * The test plan name.
+     * </pre>
+     */
+    public com.google.protobuf.ByteString
+        getTestPlanNameBytes() {
+      java.lang.Object ref = testPlanName_;
+      if (ref instanceof java.lang.String) {
+        com.google.protobuf.ByteString b = 
+            com.google.protobuf.ByteString.copyFromUtf8(
+                (java.lang.String) ref);
+        testPlanName_ = b;
+        return b;
+      } else {
+        return (com.google.protobuf.ByteString) ref;
+      }
+    }
+
+    // repeated .android.vts.UrlResourceMessage partner_report = 31;
+    public static final int PARTNER_REPORT_FIELD_NUMBER = 31;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.UrlResourceMessage> partnerReport_;
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.UrlResourceMessage> getPartnerReportList() {
+      return partnerReport_;
+    }
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder> 
+        getPartnerReportOrBuilderList() {
+      return partnerReport_;
+    }
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    public int getPartnerReportCount() {
+      return partnerReport_.size();
+    }
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.UrlResourceMessage getPartnerReport(int index) {
+      return partnerReport_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+     *
+     * <pre>
+     * Report resource flies.
+     * </pre>
+     */
+    public com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder getPartnerReportOrBuilder(
+        int index) {
+      return partnerReport_.get(index);
+    }
+
+    private void initFields() {
+      testModuleName_ = com.google.protobuf.LazyStringArrayList.EMPTY;
+      testModuleStartTimestamp_ = java.util.Collections.emptyList();
+      testPlanName_ = "";
+      partnerReport_ = java.util.Collections.emptyList();
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      for (int i = 0; i < testModuleName_.size(); i++) {
+        output.writeBytes(11, testModuleName_.getByteString(i));
+      }
+      for (int i = 0; i < testModuleStartTimestamp_.size(); i++) {
+        output.writeInt64(12, testModuleStartTimestamp_.get(i));
+      }
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(21, getTestPlanNameBytes());
+      }
+      for (int i = 0; i < partnerReport_.size(); i++) {
+        output.writeMessage(31, partnerReport_.get(i));
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      {
+        int dataSize = 0;
+        for (int i = 0; i < testModuleName_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeBytesSizeNoTag(testModuleName_.getByteString(i));
+        }
+        size += dataSize;
+        size += 1 * getTestModuleNameList().size();
+      }
+      {
+        int dataSize = 0;
+        for (int i = 0; i < testModuleStartTimestamp_.size(); i++) {
+          dataSize += com.google.protobuf.CodedOutputStream
+            .computeInt64SizeNoTag(testModuleStartTimestamp_.get(i));
+        }
+        size += dataSize;
+        size += 1 * getTestModuleStartTimestampList().size();
+      }
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(21, getTestPlanNameBytes());
+      }
+      for (int i = 0; i < partnerReport_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(31, partnerReport_.get(i));
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.TestPlanReportMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.TestPlanReportMessage}
+     *
+     * <pre>
+     * To specify a test execution report.
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestPlanReportMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestPlanReportMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.class, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+          getPartnerReportFieldBuilder();
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        testModuleName_ = com.google.protobuf.LazyStringArrayList.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        testModuleStartTimestamp_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000002);
+        testPlanName_ = "";
+        bitField0_ = (bitField0_ & ~0x00000004);
+        if (partnerReportBuilder_ == null) {
+          partnerReport_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000008);
+        } else {
+          partnerReportBuilder_.clear();
+        }
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_TestPlanReportMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage build() {
+        com.android.vts.proto.VtsReportMessage.TestPlanReportMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.TestPlanReportMessage result = new com.android.vts.proto.VtsReportMessage.TestPlanReportMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((bitField0_ & 0x00000001) == 0x00000001)) {
+          testModuleName_ = new com.google.protobuf.UnmodifiableLazyStringList(
+              testModuleName_);
+          bitField0_ = (bitField0_ & ~0x00000001);
+        }
+        result.testModuleName_ = testModuleName_;
+        if (((bitField0_ & 0x00000002) == 0x00000002)) {
+          testModuleStartTimestamp_ = java.util.Collections.unmodifiableList(testModuleStartTimestamp_);
+          bitField0_ = (bitField0_ & ~0x00000002);
+        }
+        result.testModuleStartTimestamp_ = testModuleStartTimestamp_;
+        if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.testPlanName_ = testPlanName_;
+        if (partnerReportBuilder_ == null) {
+          if (((bitField0_ & 0x00000008) == 0x00000008)) {
+            partnerReport_ = java.util.Collections.unmodifiableList(partnerReport_);
+            bitField0_ = (bitField0_ & ~0x00000008);
+          }
+          result.partnerReport_ = partnerReport_;
+        } else {
+          result.partnerReport_ = partnerReportBuilder_.build();
+        }
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.TestPlanReportMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.TestPlanReportMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.TestPlanReportMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.getDefaultInstance()) return this;
+        if (!other.testModuleName_.isEmpty()) {
+          if (testModuleName_.isEmpty()) {
+            testModuleName_ = other.testModuleName_;
+            bitField0_ = (bitField0_ & ~0x00000001);
+          } else {
+            ensureTestModuleNameIsMutable();
+            testModuleName_.addAll(other.testModuleName_);
+          }
+          onChanged();
+        }
+        if (!other.testModuleStartTimestamp_.isEmpty()) {
+          if (testModuleStartTimestamp_.isEmpty()) {
+            testModuleStartTimestamp_ = other.testModuleStartTimestamp_;
+            bitField0_ = (bitField0_ & ~0x00000002);
+          } else {
+            ensureTestModuleStartTimestampIsMutable();
+            testModuleStartTimestamp_.addAll(other.testModuleStartTimestamp_);
+          }
+          onChanged();
+        }
+        if (other.hasTestPlanName()) {
+          bitField0_ |= 0x00000004;
+          testPlanName_ = other.testPlanName_;
+          onChanged();
+        }
+        if (partnerReportBuilder_ == null) {
+          if (!other.partnerReport_.isEmpty()) {
+            if (partnerReport_.isEmpty()) {
+              partnerReport_ = other.partnerReport_;
+              bitField0_ = (bitField0_ & ~0x00000008);
+            } else {
+              ensurePartnerReportIsMutable();
+              partnerReport_.addAll(other.partnerReport_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.partnerReport_.isEmpty()) {
+            if (partnerReportBuilder_.isEmpty()) {
+              partnerReportBuilder_.dispose();
+              partnerReportBuilder_ = null;
+              partnerReport_ = other.partnerReport_;
+              bitField0_ = (bitField0_ & ~0x00000008);
+              partnerReportBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getPartnerReportFieldBuilder() : null;
+            } else {
+              partnerReportBuilder_.addAllMessages(other.partnerReport_);
+            }
+          }
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.TestPlanReportMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.TestPlanReportMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // repeated string test_module_name = 11;
+      private com.google.protobuf.LazyStringList testModuleName_ = com.google.protobuf.LazyStringArrayList.EMPTY;
+      private void ensureTestModuleNameIsMutable() {
+        if (!((bitField0_ & 0x00000001) == 0x00000001)) {
+          testModuleName_ = new com.google.protobuf.LazyStringArrayList(testModuleName_);
+          bitField0_ |= 0x00000001;
+         }
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public java.util.List<java.lang.String>
+          getTestModuleNameList() {
+        return java.util.Collections.unmodifiableList(testModuleName_);
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public int getTestModuleNameCount() {
+        return testModuleName_.size();
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public java.lang.String getTestModuleName(int index) {
+        return testModuleName_.get(index);
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString
+          getTestModuleNameBytes(int index) {
+        return testModuleName_.getByteString(index);
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public Builder setTestModuleName(
+          int index, java.lang.String value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureTestModuleNameIsMutable();
+        testModuleName_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public Builder addTestModuleName(
+          java.lang.String value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureTestModuleNameIsMutable();
+        testModuleName_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public Builder addAllTestModuleName(
+          java.lang.Iterable<java.lang.String> values) {
+        ensureTestModuleNameIsMutable();
+        super.addAll(values, testModuleName_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public Builder clearTestModuleName() {
+        testModuleName_ = com.google.protobuf.LazyStringArrayList.EMPTY;
+        bitField0_ = (bitField0_ & ~0x00000001);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated string test_module_name = 11;</code>
+       *
+       * <pre>
+       * Keys used to find all TestReportMessage messages of test modules in
+       * this plan.
+       * </pre>
+       */
+      public Builder addTestModuleNameBytes(
+          com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  ensureTestModuleNameIsMutable();
+        testModuleName_.add(value);
+        onChanged();
+        return this;
+      }
+
+      // repeated int64 test_module_start_timestamp = 12;
+      private java.util.List<java.lang.Long> testModuleStartTimestamp_ = java.util.Collections.emptyList();
+      private void ensureTestModuleStartTimestampIsMutable() {
+        if (!((bitField0_ & 0x00000002) == 0x00000002)) {
+          testModuleStartTimestamp_ = new java.util.ArrayList<java.lang.Long>(testModuleStartTimestamp_);
+          bitField0_ |= 0x00000002;
+         }
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public java.util.List<java.lang.Long>
+          getTestModuleStartTimestampList() {
+        return java.util.Collections.unmodifiableList(testModuleStartTimestamp_);
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public int getTestModuleStartTimestampCount() {
+        return testModuleStartTimestamp_.size();
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public long getTestModuleStartTimestamp(int index) {
+        return testModuleStartTimestamp_.get(index);
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public Builder setTestModuleStartTimestamp(
+          int index, long value) {
+        ensureTestModuleStartTimestampIsMutable();
+        testModuleStartTimestamp_.set(index, value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public Builder addTestModuleStartTimestamp(long value) {
+        ensureTestModuleStartTimestampIsMutable();
+        testModuleStartTimestamp_.add(value);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public Builder addAllTestModuleStartTimestamp(
+          java.lang.Iterable<? extends java.lang.Long> values) {
+        ensureTestModuleStartTimestampIsMutable();
+        super.addAll(values, testModuleStartTimestamp_);
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>repeated int64 test_module_start_timestamp = 12;</code>
+       */
+      public Builder clearTestModuleStartTimestamp() {
+        testModuleStartTimestamp_ = java.util.Collections.emptyList();
+        bitField0_ = (bitField0_ & ~0x00000002);
+        onChanged();
+        return this;
+      }
+
+      // optional string test_plan_name = 21;
+      private java.lang.Object testPlanName_ = "";
+      /**
+       * <code>optional string test_plan_name = 21;</code>
+       *
+       * <pre>
+       * The test plan name.
+       * </pre>
+       */
+      public boolean hasTestPlanName() {
+        return ((bitField0_ & 0x00000004) == 0x00000004);
+      }
+      /**
+       * <code>optional string test_plan_name = 21;</code>
+       *
+       * <pre>
+       * The test plan name.
+       * </pre>
+       */
+      public java.lang.String getTestPlanName() {
+        java.lang.Object ref = testPlanName_;
+        if (!(ref instanceof java.lang.String)) {
+          java.lang.String s = ((com.google.protobuf.ByteString) ref)
+              .toStringUtf8();
+          testPlanName_ = s;
+          return s;
+        } else {
+          return (java.lang.String) ref;
+        }
+      }
+      /**
+       * <code>optional string test_plan_name = 21;</code>
+       *
+       * <pre>
+       * The test plan name.
+       * </pre>
+       */
+      public com.google.protobuf.ByteString
+          getTestPlanNameBytes() {
+        java.lang.Object ref = testPlanName_;
+        if (ref instanceof String) {
+          com.google.protobuf.ByteString b = 
+              com.google.protobuf.ByteString.copyFromUtf8(
+                  (java.lang.String) ref);
+          testPlanName_ = b;
+          return b;
+        } else {
+          return (com.google.protobuf.ByteString) ref;
+        }
+      }
+      /**
+       * <code>optional string test_plan_name = 21;</code>
+       *
+       * <pre>
+       * The test plan name.
+       * </pre>
+       */
+      public Builder setTestPlanName(
+          java.lang.String value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        testPlanName_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional string test_plan_name = 21;</code>
+       *
+       * <pre>
+       * The test plan name.
+       * </pre>
+       */
+      public Builder clearTestPlanName() {
+        bitField0_ = (bitField0_ & ~0x00000004);
+        testPlanName_ = getDefaultInstance().getTestPlanName();
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional string test_plan_name = 21;</code>
+       *
+       * <pre>
+       * The test plan name.
+       * </pre>
+       */
+      public Builder setTestPlanNameBytes(
+          com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000004;
+        testPlanName_ = value;
+        onChanged();
+        return this;
+      }
+
+      // repeated .android.vts.UrlResourceMessage partner_report = 31;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.UrlResourceMessage> partnerReport_ =
+        java.util.Collections.emptyList();
+      private void ensurePartnerReportIsMutable() {
+        if (!((bitField0_ & 0x00000008) == 0x00000008)) {
+          partnerReport_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.UrlResourceMessage>(partnerReport_);
+          bitField0_ |= 0x00000008;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.UrlResourceMessage, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder, com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder> partnerReportBuilder_;
+
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.UrlResourceMessage> getPartnerReportList() {
+        if (partnerReportBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(partnerReport_);
+        } else {
+          return partnerReportBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public int getPartnerReportCount() {
+        if (partnerReportBuilder_ == null) {
+          return partnerReport_.size();
+        } else {
+          return partnerReportBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage getPartnerReport(int index) {
+        if (partnerReportBuilder_ == null) {
+          return partnerReport_.get(index);
+        } else {
+          return partnerReportBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder setPartnerReport(
+          int index, com.android.vts.proto.VtsReportMessage.UrlResourceMessage value) {
+        if (partnerReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensurePartnerReportIsMutable();
+          partnerReport_.set(index, value);
+          onChanged();
+        } else {
+          partnerReportBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder setPartnerReport(
+          int index, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder builderForValue) {
+        if (partnerReportBuilder_ == null) {
+          ensurePartnerReportIsMutable();
+          partnerReport_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          partnerReportBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder addPartnerReport(com.android.vts.proto.VtsReportMessage.UrlResourceMessage value) {
+        if (partnerReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensurePartnerReportIsMutable();
+          partnerReport_.add(value);
+          onChanged();
+        } else {
+          partnerReportBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder addPartnerReport(
+          int index, com.android.vts.proto.VtsReportMessage.UrlResourceMessage value) {
+        if (partnerReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensurePartnerReportIsMutable();
+          partnerReport_.add(index, value);
+          onChanged();
+        } else {
+          partnerReportBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder addPartnerReport(
+          com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder builderForValue) {
+        if (partnerReportBuilder_ == null) {
+          ensurePartnerReportIsMutable();
+          partnerReport_.add(builderForValue.build());
+          onChanged();
+        } else {
+          partnerReportBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder addPartnerReport(
+          int index, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder builderForValue) {
+        if (partnerReportBuilder_ == null) {
+          ensurePartnerReportIsMutable();
+          partnerReport_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          partnerReportBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder addAllPartnerReport(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.UrlResourceMessage> values) {
+        if (partnerReportBuilder_ == null) {
+          ensurePartnerReportIsMutable();
+          super.addAll(values, partnerReport_);
+          onChanged();
+        } else {
+          partnerReportBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder clearPartnerReport() {
+        if (partnerReportBuilder_ == null) {
+          partnerReport_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000008);
+          onChanged();
+        } else {
+          partnerReportBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public Builder removePartnerReport(int index) {
+        if (partnerReportBuilder_ == null) {
+          ensurePartnerReportIsMutable();
+          partnerReport_.remove(index);
+          onChanged();
+        } else {
+          partnerReportBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder getPartnerReportBuilder(
+          int index) {
+        return getPartnerReportFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder getPartnerReportOrBuilder(
+          int index) {
+        if (partnerReportBuilder_ == null) {
+          return partnerReport_.get(index);  } else {
+          return partnerReportBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder> 
+           getPartnerReportOrBuilderList() {
+        if (partnerReportBuilder_ != null) {
+          return partnerReportBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(partnerReport_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder addPartnerReportBuilder() {
+        return getPartnerReportFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.UrlResourceMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder addPartnerReportBuilder(
+          int index) {
+        return getPartnerReportFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.UrlResourceMessage partner_report = 31;</code>
+       *
+       * <pre>
+       * Report resource flies.
+       * </pre>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder> 
+           getPartnerReportBuilderList() {
+        return getPartnerReportFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.UrlResourceMessage, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder, com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder> 
+          getPartnerReportFieldBuilder() {
+        if (partnerReportBuilder_ == null) {
+          partnerReportBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.UrlResourceMessage, com.android.vts.proto.VtsReportMessage.UrlResourceMessage.Builder, com.android.vts.proto.VtsReportMessage.UrlResourceMessageOrBuilder>(
+                  partnerReport_,
+                  ((bitField0_ & 0x00000008) == 0x00000008),
+                  getParentForChildren(),
+                  isClean());
+          partnerReport_ = null;
+        }
+        return partnerReportBuilder_;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.TestPlanReportMessage)
+    }
+
+    static {
+      defaultInstance = new TestPlanReportMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.TestPlanReportMessage)
+  }
+
+  public interface DashboardPostMessageOrBuilder
+      extends com.google.protobuf.MessageOrBuilder {
+
+    // optional string access_token = 1;
+    /**
+     * <code>optional string access_token = 1;</code>
+     *
+     * <pre>
+     * oauth2.0 access token
+     * </pre>
+     */
+    boolean hasAccessToken();
+    /**
+     * <code>optional string access_token = 1;</code>
+     *
+     * <pre>
+     * oauth2.0 access token
+     * </pre>
+     */
+    java.lang.String getAccessToken();
+    /**
+     * <code>optional string access_token = 1;</code>
+     *
+     * <pre>
+     * oauth2.0 access token
+     * </pre>
+     */
+    com.google.protobuf.ByteString
+        getAccessTokenBytes();
+
+    // repeated .android.vts.TestReportMessage test_report = 2;
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.TestReportMessage> 
+        getTestReportList();
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    com.android.vts.proto.VtsReportMessage.TestReportMessage getTestReport(int index);
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    int getTestReportCount();
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder> 
+        getTestReportOrBuilderList();
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder getTestReportOrBuilder(
+        int index);
+
+    // repeated .android.vts.TestPlanReportMessage test_plan_report = 3;
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    java.util.List<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage> 
+        getTestPlanReportList();
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    com.android.vts.proto.VtsReportMessage.TestPlanReportMessage getTestPlanReport(int index);
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    int getTestPlanReportCount();
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder> 
+        getTestPlanReportOrBuilderList();
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder getTestPlanReportOrBuilder(
+        int index);
+  }
+  /**
+   * Protobuf type {@code android.vts.DashboardPostMessage}
+   *
+   * <pre>
+   * Proto wrapper for posting data to the VTS Dashboard
+   * </pre>
+   */
+  public static final class DashboardPostMessage extends
+      com.google.protobuf.GeneratedMessage
+      implements DashboardPostMessageOrBuilder {
+    // Use DashboardPostMessage.newBuilder() to construct.
+    private DashboardPostMessage(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
+      super(builder);
+      this.unknownFields = builder.getUnknownFields();
+    }
+    private DashboardPostMessage(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
+
+    private static final DashboardPostMessage defaultInstance;
+    public static DashboardPostMessage getDefaultInstance() {
+      return defaultInstance;
+    }
+
+    public DashboardPostMessage getDefaultInstanceForType() {
+      return defaultInstance;
+    }
+
+    private final com.google.protobuf.UnknownFieldSet unknownFields;
+    @java.lang.Override
+    public final com.google.protobuf.UnknownFieldSet
+        getUnknownFields() {
+      return this.unknownFields;
+    }
+    private DashboardPostMessage(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      initFields();
+      int mutable_bitField0_ = 0;
+      com.google.protobuf.UnknownFieldSet.Builder unknownFields =
+          com.google.protobuf.UnknownFieldSet.newBuilder();
+      try {
+        boolean done = false;
+        while (!done) {
+          int tag = input.readTag();
+          switch (tag) {
+            case 0:
+              done = true;
+              break;
+            default: {
+              if (!parseUnknownField(input, unknownFields,
+                                     extensionRegistry, tag)) {
+                done = true;
+              }
+              break;
+            }
+            case 10: {
+              bitField0_ |= 0x00000001;
+              accessToken_ = input.readBytes();
+              break;
+            }
+            case 18: {
+              if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
+                testReport_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.TestReportMessage>();
+                mutable_bitField0_ |= 0x00000002;
+              }
+              testReport_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.TestReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+            case 26: {
+              if (!((mutable_bitField0_ & 0x00000004) == 0x00000004)) {
+                testPlanReport_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage>();
+                mutable_bitField0_ |= 0x00000004;
+              }
+              testPlanReport_.add(input.readMessage(com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.PARSER, extensionRegistry));
+              break;
+            }
+          }
+        }
+      } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+        throw e.setUnfinishedMessage(this);
+      } catch (java.io.IOException e) {
+        throw new com.google.protobuf.InvalidProtocolBufferException(
+            e.getMessage()).setUnfinishedMessage(this);
+      } finally {
+        if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) {
+          testReport_ = java.util.Collections.unmodifiableList(testReport_);
+        }
+        if (((mutable_bitField0_ & 0x00000004) == 0x00000004)) {
+          testPlanReport_ = java.util.Collections.unmodifiableList(testPlanReport_);
+        }
+        this.unknownFields = unknownFields.build();
+        makeExtensionsImmutable();
+      }
+    }
+    public static final com.google.protobuf.Descriptors.Descriptor
+        getDescriptor() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_DashboardPostMessage_descriptor;
+    }
+
+    protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+        internalGetFieldAccessorTable() {
+      return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_DashboardPostMessage_fieldAccessorTable
+          .ensureFieldAccessorsInitialized(
+              com.android.vts.proto.VtsReportMessage.DashboardPostMessage.class, com.android.vts.proto.VtsReportMessage.DashboardPostMessage.Builder.class);
+    }
+
+    public static com.google.protobuf.Parser<DashboardPostMessage> PARSER =
+        new com.google.protobuf.AbstractParser<DashboardPostMessage>() {
+      public DashboardPostMessage parsePartialFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws com.google.protobuf.InvalidProtocolBufferException {
+        return new DashboardPostMessage(input, extensionRegistry);
+      }
+    };
+
+    @java.lang.Override
+    public com.google.protobuf.Parser<DashboardPostMessage> getParserForType() {
+      return PARSER;
+    }
+
+    private int bitField0_;
+    // optional string access_token = 1;
+    public static final int ACCESS_TOKEN_FIELD_NUMBER = 1;
+    private java.lang.Object accessToken_;
+    /**
+     * <code>optional string access_token = 1;</code>
+     *
+     * <pre>
+     * oauth2.0 access token
+     * </pre>
+     */
+    public boolean hasAccessToken() {
+      return ((bitField0_ & 0x00000001) == 0x00000001);
+    }
+    /**
+     * <code>optional string access_token = 1;</code>
+     *
+     * <pre>
+     * oauth2.0 access token
+     * </pre>
+     */
+    public java.lang.String getAccessToken() {
+      java.lang.Object ref = accessToken_;
+      if (ref instanceof java.lang.String) {
+        return (java.lang.String) ref;
+      } else {
+        com.google.protobuf.ByteString bs = 
+            (com.google.protobuf.ByteString) ref;
+        java.lang.String s = bs.toStringUtf8();
+        if (bs.isValidUtf8()) {
+          accessToken_ = s;
+        }
+        return s;
+      }
+    }
+    /**
+     * <code>optional string access_token = 1;</code>
+     *
+     * <pre>
+     * oauth2.0 access token
+     * </pre>
+     */
+    public com.google.protobuf.ByteString
+        getAccessTokenBytes() {
+      java.lang.Object ref = accessToken_;
+      if (ref instanceof java.lang.String) {
+        com.google.protobuf.ByteString b = 
+            com.google.protobuf.ByteString.copyFromUtf8(
+                (java.lang.String) ref);
+        accessToken_ = b;
+        return b;
+      } else {
+        return (com.google.protobuf.ByteString) ref;
+      }
+    }
+
+    // repeated .android.vts.TestReportMessage test_report = 2;
+    public static final int TEST_REPORT_FIELD_NUMBER = 2;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.TestReportMessage> testReport_;
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.TestReportMessage> getTestReportList() {
+      return testReport_;
+    }
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder> 
+        getTestReportOrBuilderList() {
+      return testReport_;
+    }
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    public int getTestReportCount() {
+      return testReport_.size();
+    }
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestReportMessage getTestReport(int index) {
+      return testReport_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder getTestReportOrBuilder(
+        int index) {
+      return testReport_.get(index);
+    }
+
+    // repeated .android.vts.TestPlanReportMessage test_plan_report = 3;
+    public static final int TEST_PLAN_REPORT_FIELD_NUMBER = 3;
+    private java.util.List<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage> testPlanReport_;
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    public java.util.List<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage> getTestPlanReportList() {
+      return testPlanReport_;
+    }
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    public java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder> 
+        getTestPlanReportOrBuilderList() {
+      return testPlanReport_;
+    }
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    public int getTestPlanReportCount() {
+      return testPlanReport_.size();
+    }
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage getTestPlanReport(int index) {
+      return testPlanReport_.get(index);
+    }
+    /**
+     * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+     */
+    public com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder getTestPlanReportOrBuilder(
+        int index) {
+      return testPlanReport_.get(index);
+    }
+
+    private void initFields() {
+      accessToken_ = "";
+      testReport_ = java.util.Collections.emptyList();
+      testPlanReport_ = java.util.Collections.emptyList();
+    }
+    private byte memoizedIsInitialized = -1;
+    public final boolean isInitialized() {
+      byte isInitialized = memoizedIsInitialized;
+      if (isInitialized != -1) return isInitialized == 1;
+
+      memoizedIsInitialized = 1;
+      return true;
+    }
+
+    public void writeTo(com.google.protobuf.CodedOutputStream output)
+                        throws java.io.IOException {
+      getSerializedSize();
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        output.writeBytes(1, getAccessTokenBytes());
+      }
+      for (int i = 0; i < testReport_.size(); i++) {
+        output.writeMessage(2, testReport_.get(i));
+      }
+      for (int i = 0; i < testPlanReport_.size(); i++) {
+        output.writeMessage(3, testPlanReport_.get(i));
+      }
+      getUnknownFields().writeTo(output);
+    }
+
+    private int memoizedSerializedSize = -1;
+    public int getSerializedSize() {
+      int size = memoizedSerializedSize;
+      if (size != -1) return size;
+
+      size = 0;
+      if (((bitField0_ & 0x00000001) == 0x00000001)) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeBytesSize(1, getAccessTokenBytes());
+      }
+      for (int i = 0; i < testReport_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(2, testReport_.get(i));
+      }
+      for (int i = 0; i < testPlanReport_.size(); i++) {
+        size += com.google.protobuf.CodedOutputStream
+          .computeMessageSize(3, testPlanReport_.get(i));
+      }
+      size += getUnknownFields().getSerializedSize();
+      memoizedSerializedSize = size;
+      return size;
+    }
+
+    private static final long serialVersionUID = 0L;
+    @java.lang.Override
+    protected java.lang.Object writeReplace()
+        throws java.io.ObjectStreamException {
+      return super.writeReplace();
+    }
+
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(
+        com.google.protobuf.ByteString data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(
+        com.google.protobuf.ByteString data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(byte[] data)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(
+        byte[] data,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws com.google.protobuf.InvalidProtocolBufferException {
+      return PARSER.parseFrom(data, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseDelimitedFrom(java.io.InputStream input)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseDelimitedFrom(
+        java.io.InputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseDelimitedFrom(input, extensionRegistry);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(
+        com.google.protobuf.CodedInputStream input)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input);
+    }
+    public static com.android.vts.proto.VtsReportMessage.DashboardPostMessage parseFrom(
+        com.google.protobuf.CodedInputStream input,
+        com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+        throws java.io.IOException {
+      return PARSER.parseFrom(input, extensionRegistry);
+    }
+
+    public static Builder newBuilder() { return Builder.create(); }
+    public Builder newBuilderForType() { return newBuilder(); }
+    public static Builder newBuilder(com.android.vts.proto.VtsReportMessage.DashboardPostMessage prototype) {
+      return newBuilder().mergeFrom(prototype);
+    }
+    public Builder toBuilder() { return newBuilder(this); }
+
+    @java.lang.Override
+    protected Builder newBuilderForType(
+        com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+      Builder builder = new Builder(parent);
+      return builder;
+    }
+    /**
+     * Protobuf type {@code android.vts.DashboardPostMessage}
+     *
+     * <pre>
+     * Proto wrapper for posting data to the VTS Dashboard
+     * </pre>
+     */
+    public static final class Builder extends
+        com.google.protobuf.GeneratedMessage.Builder<Builder>
+       implements com.android.vts.proto.VtsReportMessage.DashboardPostMessageOrBuilder {
+      public static final com.google.protobuf.Descriptors.Descriptor
+          getDescriptor() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_DashboardPostMessage_descriptor;
+      }
+
+      protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
+          internalGetFieldAccessorTable() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_DashboardPostMessage_fieldAccessorTable
+            .ensureFieldAccessorsInitialized(
+                com.android.vts.proto.VtsReportMessage.DashboardPostMessage.class, com.android.vts.proto.VtsReportMessage.DashboardPostMessage.Builder.class);
+      }
+
+      // Construct using com.android.vts.proto.VtsReportMessage.DashboardPostMessage.newBuilder()
+      private Builder() {
+        maybeForceBuilderInitialization();
+      }
+
+      private Builder(
+          com.google.protobuf.GeneratedMessage.BuilderParent parent) {
+        super(parent);
+        maybeForceBuilderInitialization();
+      }
+      private void maybeForceBuilderInitialization() {
+        if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
+          getTestReportFieldBuilder();
+          getTestPlanReportFieldBuilder();
+        }
+      }
+      private static Builder create() {
+        return new Builder();
+      }
+
+      public Builder clear() {
+        super.clear();
+        accessToken_ = "";
+        bitField0_ = (bitField0_ & ~0x00000001);
+        if (testReportBuilder_ == null) {
+          testReport_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000002);
+        } else {
+          testReportBuilder_.clear();
+        }
+        if (testPlanReportBuilder_ == null) {
+          testPlanReport_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000004);
+        } else {
+          testPlanReportBuilder_.clear();
+        }
+        return this;
+      }
+
+      public Builder clone() {
+        return create().mergeFrom(buildPartial());
+      }
+
+      public com.google.protobuf.Descriptors.Descriptor
+          getDescriptorForType() {
+        return com.android.vts.proto.VtsReportMessage.internal_static_android_vts_DashboardPostMessage_descriptor;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.DashboardPostMessage getDefaultInstanceForType() {
+        return com.android.vts.proto.VtsReportMessage.DashboardPostMessage.getDefaultInstance();
+      }
+
+      public com.android.vts.proto.VtsReportMessage.DashboardPostMessage build() {
+        com.android.vts.proto.VtsReportMessage.DashboardPostMessage result = buildPartial();
+        if (!result.isInitialized()) {
+          throw newUninitializedMessageException(result);
+        }
+        return result;
+      }
+
+      public com.android.vts.proto.VtsReportMessage.DashboardPostMessage buildPartial() {
+        com.android.vts.proto.VtsReportMessage.DashboardPostMessage result = new com.android.vts.proto.VtsReportMessage.DashboardPostMessage(this);
+        int from_bitField0_ = bitField0_;
+        int to_bitField0_ = 0;
+        if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
+          to_bitField0_ |= 0x00000001;
+        }
+        result.accessToken_ = accessToken_;
+        if (testReportBuilder_ == null) {
+          if (((bitField0_ & 0x00000002) == 0x00000002)) {
+            testReport_ = java.util.Collections.unmodifiableList(testReport_);
+            bitField0_ = (bitField0_ & ~0x00000002);
+          }
+          result.testReport_ = testReport_;
+        } else {
+          result.testReport_ = testReportBuilder_.build();
+        }
+        if (testPlanReportBuilder_ == null) {
+          if (((bitField0_ & 0x00000004) == 0x00000004)) {
+            testPlanReport_ = java.util.Collections.unmodifiableList(testPlanReport_);
+            bitField0_ = (bitField0_ & ~0x00000004);
+          }
+          result.testPlanReport_ = testPlanReport_;
+        } else {
+          result.testPlanReport_ = testPlanReportBuilder_.build();
+        }
+        result.bitField0_ = to_bitField0_;
+        onBuilt();
+        return result;
+      }
+
+      public Builder mergeFrom(com.google.protobuf.Message other) {
+        if (other instanceof com.android.vts.proto.VtsReportMessage.DashboardPostMessage) {
+          return mergeFrom((com.android.vts.proto.VtsReportMessage.DashboardPostMessage)other);
+        } else {
+          super.mergeFrom(other);
+          return this;
+        }
+      }
+
+      public Builder mergeFrom(com.android.vts.proto.VtsReportMessage.DashboardPostMessage other) {
+        if (other == com.android.vts.proto.VtsReportMessage.DashboardPostMessage.getDefaultInstance()) return this;
+        if (other.hasAccessToken()) {
+          bitField0_ |= 0x00000001;
+          accessToken_ = other.accessToken_;
+          onChanged();
+        }
+        if (testReportBuilder_ == null) {
+          if (!other.testReport_.isEmpty()) {
+            if (testReport_.isEmpty()) {
+              testReport_ = other.testReport_;
+              bitField0_ = (bitField0_ & ~0x00000002);
+            } else {
+              ensureTestReportIsMutable();
+              testReport_.addAll(other.testReport_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.testReport_.isEmpty()) {
+            if (testReportBuilder_.isEmpty()) {
+              testReportBuilder_.dispose();
+              testReportBuilder_ = null;
+              testReport_ = other.testReport_;
+              bitField0_ = (bitField0_ & ~0x00000002);
+              testReportBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getTestReportFieldBuilder() : null;
+            } else {
+              testReportBuilder_.addAllMessages(other.testReport_);
+            }
+          }
+        }
+        if (testPlanReportBuilder_ == null) {
+          if (!other.testPlanReport_.isEmpty()) {
+            if (testPlanReport_.isEmpty()) {
+              testPlanReport_ = other.testPlanReport_;
+              bitField0_ = (bitField0_ & ~0x00000004);
+            } else {
+              ensureTestPlanReportIsMutable();
+              testPlanReport_.addAll(other.testPlanReport_);
+            }
+            onChanged();
+          }
+        } else {
+          if (!other.testPlanReport_.isEmpty()) {
+            if (testPlanReportBuilder_.isEmpty()) {
+              testPlanReportBuilder_.dispose();
+              testPlanReportBuilder_ = null;
+              testPlanReport_ = other.testPlanReport_;
+              bitField0_ = (bitField0_ & ~0x00000004);
+              testPlanReportBuilder_ = 
+                com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders ?
+                   getTestPlanReportFieldBuilder() : null;
+            } else {
+              testPlanReportBuilder_.addAllMessages(other.testPlanReport_);
+            }
+          }
+        }
+        this.mergeUnknownFields(other.getUnknownFields());
+        return this;
+      }
+
+      public final boolean isInitialized() {
+        return true;
+      }
+
+      public Builder mergeFrom(
+          com.google.protobuf.CodedInputStream input,
+          com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+          throws java.io.IOException {
+        com.android.vts.proto.VtsReportMessage.DashboardPostMessage parsedMessage = null;
+        try {
+          parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry);
+        } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+          parsedMessage = (com.android.vts.proto.VtsReportMessage.DashboardPostMessage) e.getUnfinishedMessage();
+          throw e;
+        } finally {
+          if (parsedMessage != null) {
+            mergeFrom(parsedMessage);
+          }
+        }
+        return this;
+      }
+      private int bitField0_;
+
+      // optional string access_token = 1;
+      private java.lang.Object accessToken_ = "";
+      /**
+       * <code>optional string access_token = 1;</code>
+       *
+       * <pre>
+       * oauth2.0 access token
+       * </pre>
+       */
+      public boolean hasAccessToken() {
+        return ((bitField0_ & 0x00000001) == 0x00000001);
+      }
+      /**
+       * <code>optional string access_token = 1;</code>
+       *
+       * <pre>
+       * oauth2.0 access token
+       * </pre>
+       */
+      public java.lang.String getAccessToken() {
+        java.lang.Object ref = accessToken_;
+        if (!(ref instanceof java.lang.String)) {
+          java.lang.String s = ((com.google.protobuf.ByteString) ref)
+              .toStringUtf8();
+          accessToken_ = s;
+          return s;
+        } else {
+          return (java.lang.String) ref;
+        }
+      }
+      /**
+       * <code>optional string access_token = 1;</code>
+       *
+       * <pre>
+       * oauth2.0 access token
+       * </pre>
+       */
+      public com.google.protobuf.ByteString
+          getAccessTokenBytes() {
+        java.lang.Object ref = accessToken_;
+        if (ref instanceof String) {
+          com.google.protobuf.ByteString b = 
+              com.google.protobuf.ByteString.copyFromUtf8(
+                  (java.lang.String) ref);
+          accessToken_ = b;
+          return b;
+        } else {
+          return (com.google.protobuf.ByteString) ref;
+        }
+      }
+      /**
+       * <code>optional string access_token = 1;</code>
+       *
+       * <pre>
+       * oauth2.0 access token
+       * </pre>
+       */
+      public Builder setAccessToken(
+          java.lang.String value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        accessToken_ = value;
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional string access_token = 1;</code>
+       *
+       * <pre>
+       * oauth2.0 access token
+       * </pre>
+       */
+      public Builder clearAccessToken() {
+        bitField0_ = (bitField0_ & ~0x00000001);
+        accessToken_ = getDefaultInstance().getAccessToken();
+        onChanged();
+        return this;
+      }
+      /**
+       * <code>optional string access_token = 1;</code>
+       *
+       * <pre>
+       * oauth2.0 access token
+       * </pre>
+       */
+      public Builder setAccessTokenBytes(
+          com.google.protobuf.ByteString value) {
+        if (value == null) {
+    throw new NullPointerException();
+  }
+  bitField0_ |= 0x00000001;
+        accessToken_ = value;
+        onChanged();
+        return this;
+      }
+
+      // repeated .android.vts.TestReportMessage test_report = 2;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.TestReportMessage> testReport_ =
+        java.util.Collections.emptyList();
+      private void ensureTestReportIsMutable() {
+        if (!((bitField0_ & 0x00000002) == 0x00000002)) {
+          testReport_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.TestReportMessage>(testReport_);
+          bitField0_ |= 0x00000002;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.TestReportMessage, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder> testReportBuilder_;
+
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.TestReportMessage> getTestReportList() {
+        if (testReportBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(testReport_);
+        } else {
+          return testReportBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public int getTestReportCount() {
+        if (testReportBuilder_ == null) {
+          return testReport_.size();
+        } else {
+          return testReportBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage getTestReport(int index) {
+        if (testReportBuilder_ == null) {
+          return testReport_.get(index);
+        } else {
+          return testReportBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder setTestReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestReportMessage value) {
+        if (testReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestReportIsMutable();
+          testReport_.set(index, value);
+          onChanged();
+        } else {
+          testReportBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder setTestReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder builderForValue) {
+        if (testReportBuilder_ == null) {
+          ensureTestReportIsMutable();
+          testReport_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          testReportBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder addTestReport(com.android.vts.proto.VtsReportMessage.TestReportMessage value) {
+        if (testReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestReportIsMutable();
+          testReport_.add(value);
+          onChanged();
+        } else {
+          testReportBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder addTestReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestReportMessage value) {
+        if (testReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestReportIsMutable();
+          testReport_.add(index, value);
+          onChanged();
+        } else {
+          testReportBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder addTestReport(
+          com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder builderForValue) {
+        if (testReportBuilder_ == null) {
+          ensureTestReportIsMutable();
+          testReport_.add(builderForValue.build());
+          onChanged();
+        } else {
+          testReportBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder addTestReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder builderForValue) {
+        if (testReportBuilder_ == null) {
+          ensureTestReportIsMutable();
+          testReport_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          testReportBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder addAllTestReport(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.TestReportMessage> values) {
+        if (testReportBuilder_ == null) {
+          ensureTestReportIsMutable();
+          super.addAll(values, testReport_);
+          onChanged();
+        } else {
+          testReportBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder clearTestReport() {
+        if (testReportBuilder_ == null) {
+          testReport_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000002);
+          onChanged();
+        } else {
+          testReportBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public Builder removeTestReport(int index) {
+        if (testReportBuilder_ == null) {
+          ensureTestReportIsMutable();
+          testReport_.remove(index);
+          onChanged();
+        } else {
+          testReportBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder getTestReportBuilder(
+          int index) {
+        return getTestReportFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder getTestReportOrBuilder(
+          int index) {
+        if (testReportBuilder_ == null) {
+          return testReport_.get(index);  } else {
+          return testReportBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder> 
+           getTestReportOrBuilderList() {
+        if (testReportBuilder_ != null) {
+          return testReportBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(testReport_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder addTestReportBuilder() {
+        return getTestReportFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.TestReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder addTestReportBuilder(
+          int index) {
+        return getTestReportFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.TestReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.TestReportMessage test_report = 2;</code>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder> 
+           getTestReportBuilderList() {
+        return getTestReportFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.TestReportMessage, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder> 
+          getTestReportFieldBuilder() {
+        if (testReportBuilder_ == null) {
+          testReportBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.TestReportMessage, com.android.vts.proto.VtsReportMessage.TestReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestReportMessageOrBuilder>(
+                  testReport_,
+                  ((bitField0_ & 0x00000002) == 0x00000002),
+                  getParentForChildren(),
+                  isClean());
+          testReport_ = null;
+        }
+        return testReportBuilder_;
+      }
+
+      // repeated .android.vts.TestPlanReportMessage test_plan_report = 3;
+      private java.util.List<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage> testPlanReport_ =
+        java.util.Collections.emptyList();
+      private void ensureTestPlanReportIsMutable() {
+        if (!((bitField0_ & 0x00000004) == 0x00000004)) {
+          testPlanReport_ = new java.util.ArrayList<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage>(testPlanReport_);
+          bitField0_ |= 0x00000004;
+         }
+      }
+
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.TestPlanReportMessage, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder> testPlanReportBuilder_;
+
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage> getTestPlanReportList() {
+        if (testPlanReportBuilder_ == null) {
+          return java.util.Collections.unmodifiableList(testPlanReport_);
+        } else {
+          return testPlanReportBuilder_.getMessageList();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public int getTestPlanReportCount() {
+        if (testPlanReportBuilder_ == null) {
+          return testPlanReport_.size();
+        } else {
+          return testPlanReportBuilder_.getCount();
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage getTestPlanReport(int index) {
+        if (testPlanReportBuilder_ == null) {
+          return testPlanReport_.get(index);
+        } else {
+          return testPlanReportBuilder_.getMessage(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder setTestPlanReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage value) {
+        if (testPlanReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.set(index, value);
+          onChanged();
+        } else {
+          testPlanReportBuilder_.setMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder setTestPlanReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder builderForValue) {
+        if (testPlanReportBuilder_ == null) {
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.set(index, builderForValue.build());
+          onChanged();
+        } else {
+          testPlanReportBuilder_.setMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder addTestPlanReport(com.android.vts.proto.VtsReportMessage.TestPlanReportMessage value) {
+        if (testPlanReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.add(value);
+          onChanged();
+        } else {
+          testPlanReportBuilder_.addMessage(value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder addTestPlanReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage value) {
+        if (testPlanReportBuilder_ == null) {
+          if (value == null) {
+            throw new NullPointerException();
+          }
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.add(index, value);
+          onChanged();
+        } else {
+          testPlanReportBuilder_.addMessage(index, value);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder addTestPlanReport(
+          com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder builderForValue) {
+        if (testPlanReportBuilder_ == null) {
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.add(builderForValue.build());
+          onChanged();
+        } else {
+          testPlanReportBuilder_.addMessage(builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder addTestPlanReport(
+          int index, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder builderForValue) {
+        if (testPlanReportBuilder_ == null) {
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.add(index, builderForValue.build());
+          onChanged();
+        } else {
+          testPlanReportBuilder_.addMessage(index, builderForValue.build());
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder addAllTestPlanReport(
+          java.lang.Iterable<? extends com.android.vts.proto.VtsReportMessage.TestPlanReportMessage> values) {
+        if (testPlanReportBuilder_ == null) {
+          ensureTestPlanReportIsMutable();
+          super.addAll(values, testPlanReport_);
+          onChanged();
+        } else {
+          testPlanReportBuilder_.addAllMessages(values);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder clearTestPlanReport() {
+        if (testPlanReportBuilder_ == null) {
+          testPlanReport_ = java.util.Collections.emptyList();
+          bitField0_ = (bitField0_ & ~0x00000004);
+          onChanged();
+        } else {
+          testPlanReportBuilder_.clear();
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public Builder removeTestPlanReport(int index) {
+        if (testPlanReportBuilder_ == null) {
+          ensureTestPlanReportIsMutable();
+          testPlanReport_.remove(index);
+          onChanged();
+        } else {
+          testPlanReportBuilder_.remove(index);
+        }
+        return this;
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder getTestPlanReportBuilder(
+          int index) {
+        return getTestPlanReportFieldBuilder().getBuilder(index);
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder getTestPlanReportOrBuilder(
+          int index) {
+        if (testPlanReportBuilder_ == null) {
+          return testPlanReport_.get(index);  } else {
+          return testPlanReportBuilder_.getMessageOrBuilder(index);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public java.util.List<? extends com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder> 
+           getTestPlanReportOrBuilderList() {
+        if (testPlanReportBuilder_ != null) {
+          return testPlanReportBuilder_.getMessageOrBuilderList();
+        } else {
+          return java.util.Collections.unmodifiableList(testPlanReport_);
+        }
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder addTestPlanReportBuilder() {
+        return getTestPlanReportFieldBuilder().addBuilder(
+            com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder addTestPlanReportBuilder(
+          int index) {
+        return getTestPlanReportFieldBuilder().addBuilder(
+            index, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.getDefaultInstance());
+      }
+      /**
+       * <code>repeated .android.vts.TestPlanReportMessage test_plan_report = 3;</code>
+       */
+      public java.util.List<com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder> 
+           getTestPlanReportBuilderList() {
+        return getTestPlanReportFieldBuilder().getBuilderList();
+      }
+      private com.google.protobuf.RepeatedFieldBuilder<
+          com.android.vts.proto.VtsReportMessage.TestPlanReportMessage, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder> 
+          getTestPlanReportFieldBuilder() {
+        if (testPlanReportBuilder_ == null) {
+          testPlanReportBuilder_ = new com.google.protobuf.RepeatedFieldBuilder<
+              com.android.vts.proto.VtsReportMessage.TestPlanReportMessage, com.android.vts.proto.VtsReportMessage.TestPlanReportMessage.Builder, com.android.vts.proto.VtsReportMessage.TestPlanReportMessageOrBuilder>(
+                  testPlanReport_,
+                  ((bitField0_ & 0x00000004) == 0x00000004),
+                  getParentForChildren(),
+                  isClean());
+          testPlanReport_ = null;
+        }
+        return testPlanReportBuilder_;
+      }
+
+      // @@protoc_insertion_point(builder_scope:android.vts.DashboardPostMessage)
+    }
+
+    static {
+      defaultInstance = new DashboardPostMessage(true);
+      defaultInstance.initFields();
+    }
+
+    // @@protoc_insertion_point(class_scope:android.vts.DashboardPostMessage)
+  }
+
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_AndroidDeviceInfoMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_AndroidDeviceInfoMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_AndroidBuildInfo_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_AndroidBuildInfo_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_VtsHostInfo_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_VtsHostInfo_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_TestCaseReportMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_TestCaseReportMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_ProfilingReportMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_ProfilingReportMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_SystraceReportMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_SystraceReportMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_CoverageReportMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_CoverageReportMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_LogMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_LogMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_UrlResourceMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_UrlResourceMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_TestReportMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_TestReportMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_TestPlanReportMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_TestPlanReportMessage_fieldAccessorTable;
+  private static com.google.protobuf.Descriptors.Descriptor
+    internal_static_android_vts_DashboardPostMessage_descriptor;
+  private static
+    com.google.protobuf.GeneratedMessage.FieldAccessorTable
+      internal_static_android_vts_DashboardPostMessage_fieldAccessorTable;
+
+  public static com.google.protobuf.Descriptors.FileDescriptor
+      getDescriptor() {
+    return descriptor;
+  }
+  private static com.google.protobuf.Descriptors.FileDescriptor
+      descriptor;
+  static {
+    java.lang.String[] descriptorData = {
+      "\n\034proto/VtsReportMessage.proto\022\013android." +
+      "vts\"\340\001\n\030AndroidDeviceInfoMessage\022\024\n\014prod" +
+      "uct_type\030\001 \001(\014\022\027\n\017product_variant\030\002 \001(\014\022" +
+      "\024\n\014build_flavor\030\013 \001(\014\022\020\n\010build_id\030\014 \001(\014\022" +
+      "\016\n\006branch\030\025 \001(\014\022\023\n\013build_alias\030\026 \001(\014\022\021\n\t" +
+      "api_level\030\037 \001(\014\022\020\n\010abi_name\0303 \001(\014\022\023\n\013abi" +
+      "_bitness\0304 \001(\014\022\016\n\006serial\030e \001(\014\"g\n\020Androi" +
+      "dBuildInfo\022\n\n\002id\030\001 \001(\014\022\014\n\004name\030\013 \001(\014\022\022\n\n" +
+      "build_type\030\014 \001(\014\022\016\n\006branch\030\r \001(\014\022\025\n\rbuil" +
+      "d_summary\030\025 \001(\014\"\037\n\013VtsHostInfo\022\020\n\010hostna",
+      "me\030\001 \001(\014\"\321\002\n\025TestCaseReportMessage\022\014\n\004na" +
+      "me\030\001 \001(\014\0220\n\013test_result\030\013 \001(\0162\033.android." +
+      "vts.TestCaseResult\022\027\n\017start_timestamp\030\025 " +
+      "\001(\003\022\025\n\rend_timestamp\030\026 \001(\003\0224\n\010coverage\030\037" +
+      " \003(\0132\".android.vts.CoverageReportMessage" +
+      "\0226\n\tprofiling\030) \003(\0132#.android.vts.Profil" +
+      "ingReportMessage\0224\n\010systrace\030* \003(\0132\".and" +
+      "roid.vts.SystraceReportMessage\022$\n\003log\030e " +
+      "\003(\0132\027.android.vts.LogMessage\"\240\002\n\026Profili" +
+      "ngReportMessage\022\014\n\004name\030\001 \001(\014\022+\n\004type\030\002 ",
+      "\001(\0162\035.android.vts.VtsProfilingType\022@\n\017re" +
+      "gression_mode\030\003 \001(\0162\'.android.vts.VtsPro" +
+      "filingRegressionMode\022\027\n\017start_timestamp\030" +
+      "\013 \001(\003\022\025\n\rend_timestamp\030\014 \001(\003\022\r\n\005label\030\025 " +
+      "\003(\014\022\r\n\005value\030\026 \003(\003\022\024\n\014x_axis_label\030\037 \001(\014" +
+      "\022\024\n\014y_axis_label\030  \001(\014\022\017\n\007options\030) \003(\014\"" +
+      "H\n\025SystraceReportMessage\022\024\n\014process_name" +
+      "\030\001 \001(\014\022\014\n\004html\030\013 \003(\014\022\013\n\003url\030\025 \003(\014\"\345\001\n\025Co" +
+      "verageReportMessage\022\021\n\tfile_path\030\013 \001(\014\022\024" +
+      "\n\014project_name\030\014 \001(\014\022\020\n\010revision\030\r \001(\014\022\034",
+      "\n\024line_coverage_vector\030\027 \003(\005\022\030\n\020total_li" +
+      "ne_count\030e \001(\005\022\032\n\022covered_line_count\030f \001" +
+      "(\005\022\024\n\010dir_path\030\001 \001(\014B\002\030\001\022\025\n\tfile_name\030\002 " +
+      "\001(\014B\002\030\001\022\020\n\004html\030\003 \001(\014B\002\030\001\"8\n\nLogMessage\022" +
+      "\013\n\003url\030\001 \001(\014\022\014\n\004name\030\002 \001(\014\022\017\n\007content\030\003 " +
+      "\001(\014\"@\n\022UrlResourceMessage\022\013\n\003url\030\001 \001(\014\022\014" +
+      "\n\004name\030\002 \001(\014\022\017\n\007content\030\003 \001(\014\"\316\004\n\021TestRe" +
+      "portMessage\022\026\n\ntest_suite\030\001 \001(\014B\002\030\001\022\014\n\004t" +
+      "est\030\002 \001(\014\022+\n\ttest_type\030\003 \001(\0162\030.android.v" +
+      "ts.VtsTestType\022:\n\013device_info\030\004 \003(\0132%.an",
+      "droid.vts.AndroidDeviceInfoMessage\0221\n\nbu" +
+      "ild_info\030\005 \001(\0132\035.android.vts.AndroidBuil" +
+      "dInfo\022\030\n\020subscriber_email\030\006 \003(\014\022+\n\thost_" +
+      "info\030\007 \001(\0132\030.android.vts.VtsHostInfo\0225\n\t" +
+      "test_case\030\013 \003(\0132\".android.vts.TestCaseRe" +
+      "portMessage\0226\n\tprofiling\030\025 \003(\0132#.android" +
+      ".vts.ProfilingReportMessage\0224\n\010systrace\030" +
+      "\026 \003(\0132\".android.vts.SystraceReportMessag" +
+      "e\022\027\n\017start_timestamp\030e \001(\003\022\025\n\rend_timest" +
+      "amp\030f \001(\003\0224\n\010coverage\030g \003(\0132\".android.vt",
+      "s.CoverageReportMessage\022%\n\003log\030\351\007 \003(\0132\027." +
+      "android.vts.LogMessage\"\247\001\n\025TestPlanRepor" +
+      "tMessage\022\030\n\020test_module_name\030\013 \003(\t\022#\n\033te" +
+      "st_module_start_timestamp\030\014 \003(\003\022\026\n\016test_" +
+      "plan_name\030\025 \001(\t\0227\n\016partner_report\030\037 \003(\0132" +
+      "\037.android.vts.UrlResourceMessage\"\237\001\n\024Das" +
+      "hboardPostMessage\022\024\n\014access_token\030\001 \001(\t\022" +
+      "3\n\013test_report\030\002 \003(\0132\036.android.vts.TestR" +
+      "eportMessage\022<\n\020test_plan_report\030\003 \003(\0132\"" +
+      ".android.vts.TestPlanReportMessage*\263\001\n\016T",
+      "estCaseResult\022\022\n\016UNKNOWN_RESULT\020\000\022\031\n\025TES" +
+      "T_CASE_RESULT_PASS\020\001\022\031\n\025TEST_CASE_RESULT" +
+      "_FAIL\020\002\022\031\n\025TEST_CASE_RESULT_SKIP\020\003\022\036\n\032TE" +
+      "ST_CASE_RESULT_EXCEPTION\020\004\022\034\n\030TEST_CASE_" +
+      "RESULT_TIMEOUT\020\005*\234\001\n\013VtsTestType\022\030\n\024UNKN" +
+      "OWN_VTS_TESTTYPE\020\000\022\036\n\032VTS_HOST_DRIVEN_ST" +
+      "RUCTURAL\020\001\022\033\n\027VTS_HOST_DRIVEN_FUZZING\020\002\022" +
+      "\031\n\025VTS_TARGET_SIDE_GTEST\020\003\022\033\n\027VTS_TARGET" +
+      "_SIDE_FUZZING\020\004*\243\001\n\032VtsProfilingRegressi" +
+      "onMode\022\033\n\027UNKNOWN_REGRESSION_MODE\020\000\022 \n\034V",
+      "TS_REGRESSION_MODE_DISABLED\020\001\022\"\n\036VTS_REG" +
+      "RESSION_MODE_INCREASING\020\002\022\"\n\036VTS_REGRESS" +
+      "ION_MODE_DECREASING\020\003*\244\001\n\020VtsProfilingTy" +
+      "pe\022\036\n\032UNKNOWN_VTS_PROFILING_TYPE\020\000\022 \n\034VT" +
+      "S_PROFILING_TYPE_TIMESTAMP\020\001\022%\n!VTS_PROF" +
+      "ILING_TYPE_LABELED_VECTOR\020\002\022\'\n#VTS_PROFI" +
+      "LING_TYPE_UNLABELED_VECTOR\020\003B+\n\025com.andr" +
+      "oid.vts.protoB\020VtsReportMessageP\000"
+    };
+    com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
+      new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
+        public com.google.protobuf.ExtensionRegistry assignDescriptors(
+            com.google.protobuf.Descriptors.FileDescriptor root) {
+          descriptor = root;
+          internal_static_android_vts_AndroidDeviceInfoMessage_descriptor =
+            getDescriptor().getMessageTypes().get(0);
+          internal_static_android_vts_AndroidDeviceInfoMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_AndroidDeviceInfoMessage_descriptor,
+              new java.lang.String[] { "ProductType", "ProductVariant", "BuildFlavor", "BuildId", "Branch", "BuildAlias", "ApiLevel", "AbiName", "AbiBitness", "Serial", });
+          internal_static_android_vts_AndroidBuildInfo_descriptor =
+            getDescriptor().getMessageTypes().get(1);
+          internal_static_android_vts_AndroidBuildInfo_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_AndroidBuildInfo_descriptor,
+              new java.lang.String[] { "Id", "Name", "BuildType", "Branch", "BuildSummary", });
+          internal_static_android_vts_VtsHostInfo_descriptor =
+            getDescriptor().getMessageTypes().get(2);
+          internal_static_android_vts_VtsHostInfo_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_VtsHostInfo_descriptor,
+              new java.lang.String[] { "Hostname", });
+          internal_static_android_vts_TestCaseReportMessage_descriptor =
+            getDescriptor().getMessageTypes().get(3);
+          internal_static_android_vts_TestCaseReportMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_TestCaseReportMessage_descriptor,
+              new java.lang.String[] { "Name", "TestResult", "StartTimestamp", "EndTimestamp", "Coverage", "Profiling", "Systrace", "Log", });
+          internal_static_android_vts_ProfilingReportMessage_descriptor =
+            getDescriptor().getMessageTypes().get(4);
+          internal_static_android_vts_ProfilingReportMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_ProfilingReportMessage_descriptor,
+              new java.lang.String[] { "Name", "Type", "RegressionMode", "StartTimestamp", "EndTimestamp", "Label", "Value", "XAxisLabel", "YAxisLabel", "Options", });
+          internal_static_android_vts_SystraceReportMessage_descriptor =
+            getDescriptor().getMessageTypes().get(5);
+          internal_static_android_vts_SystraceReportMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_SystraceReportMessage_descriptor,
+              new java.lang.String[] { "ProcessName", "Html", "Url", });
+          internal_static_android_vts_CoverageReportMessage_descriptor =
+            getDescriptor().getMessageTypes().get(6);
+          internal_static_android_vts_CoverageReportMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_CoverageReportMessage_descriptor,
+              new java.lang.String[] { "FilePath", "ProjectName", "Revision", "LineCoverageVector", "TotalLineCount", "CoveredLineCount", "DirPath", "FileName", "Html", });
+          internal_static_android_vts_LogMessage_descriptor =
+            getDescriptor().getMessageTypes().get(7);
+          internal_static_android_vts_LogMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_LogMessage_descriptor,
+              new java.lang.String[] { "Url", "Name", "Content", });
+          internal_static_android_vts_UrlResourceMessage_descriptor =
+            getDescriptor().getMessageTypes().get(8);
+          internal_static_android_vts_UrlResourceMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_UrlResourceMessage_descriptor,
+              new java.lang.String[] { "Url", "Name", "Content", });
+          internal_static_android_vts_TestReportMessage_descriptor =
+            getDescriptor().getMessageTypes().get(9);
+          internal_static_android_vts_TestReportMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_TestReportMessage_descriptor,
+              new java.lang.String[] { "TestSuite", "Test", "TestType", "DeviceInfo", "BuildInfo", "SubscriberEmail", "HostInfo", "TestCase", "Profiling", "Systrace", "StartTimestamp", "EndTimestamp", "Coverage", "Log", });
+          internal_static_android_vts_TestPlanReportMessage_descriptor =
+            getDescriptor().getMessageTypes().get(10);
+          internal_static_android_vts_TestPlanReportMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_TestPlanReportMessage_descriptor,
+              new java.lang.String[] { "TestModuleName", "TestModuleStartTimestamp", "TestPlanName", "PartnerReport", });
+          internal_static_android_vts_DashboardPostMessage_descriptor =
+            getDescriptor().getMessageTypes().get(11);
+          internal_static_android_vts_DashboardPostMessage_fieldAccessorTable = new
+            com.google.protobuf.GeneratedMessage.FieldAccessorTable(
+              internal_static_android_vts_DashboardPostMessage_descriptor,
+              new java.lang.String[] { "AccessToken", "TestReport", "TestPlanReport", });
+          return null;
+        }
+      };
+    com.google.protobuf.Descriptors.FileDescriptor
+      .internalBuildGeneratedFileFrom(descriptorData,
+        new com.google.protobuf.Descriptors.FileDescriptor[] {
+        }, assigner);
+  }
+
+  // @@protoc_insertion_point(outer_class_scope)
+}
diff --git a/src/main/java/com/android/vts/servlet/BaseServlet.java b/src/main/java/com/android/vts/servlet/BaseServlet.java
new file mode 100644
index 0000000..3fdca0a
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/BaseServlet.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class BaseServlet extends HttpServlet {
+    protected final Logger logger = Logger.getLogger(getClass().getName());
+
+    // Environment variables
+    protected static final String GERRIT_URI = System.getProperty("GERRIT_URI");
+    protected static final String GERRIT_SCOPE = System.getProperty("GERRIT_SCOPE");
+    protected static final String CLIENT_ID = System.getProperty("CLIENT_ID");
+    protected static final String ANALYTICS_ID = System.getProperty("ANALYTICS_ID");
+
+    public enum PageType {
+        TOT("ToT", "/"),
+        RELEASE("Release", "/show_release"),
+        COVERAGE_OVERVIEW("Coverage", "/show_coverage_overview"),
+        TABLE("", "/show_table"),
+        TREE("", "/show_tree"),
+        GRAPH("Profiling", "/show_graph"),
+        COVERAGE("Coverage", "/show_coverage"),
+        PERFORMANCE("Performance Digest", "/show_performance_digest"),
+        PLAN_RELEASE("", "/show_plan_release"),
+        PLAN_RUN("Plan Run", "/show_plan_run");
+
+        public final String defaultName;
+        public final String defaultUrl;
+
+        PageType(String defaultName, String defaultUrl) {
+            this.defaultName = defaultName;
+            this.defaultUrl = defaultUrl;
+        }
+    }
+
+    public static class Page {
+        private final PageType type;
+        private final String name;
+        private final String url;
+
+        public Page(PageType type) {
+            this.type = type;
+            this.name = type.defaultName;
+            this.url = type.defaultUrl;
+        }
+
+        public Page(PageType type, String name, String url) {
+            this.type = type;
+            this.name = type.defaultName + name;
+            this.url = type.defaultUrl + url;
+        }
+
+        public Page(PageType type, String url) {
+            this.type = type;
+            this.name = type.defaultName;
+            this.url = type.defaultUrl + url;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+    }
+
+    public static final List<Page> navbarLinks;
+
+    static {
+        List<Page> links = new ArrayList<>();
+        links.add(new Page(PageType.TOT));
+        links.add(new Page(PageType.RELEASE));
+        links.add(new Page(PageType.COVERAGE_OVERVIEW));
+        navbarLinks = links;
+    }
+
+    public abstract PageType getNavParentType();
+
+    /**
+     * Get a list of URL/Display name pairs for the breadcrumb hierarchy.
+     *
+     * @param request The HttpServletRequest object for the page request.
+     * @return a list of Page entries.
+     */
+    public abstract List<Page> getBreadcrumbLinks(HttpServletRequest request);
+
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        // If the user is logged out, allow them to log back in and return to the page.
+        // Set the logout URL to direct back to a login page that directs to the current request.
+        UserService userService = UserServiceFactory.getUserService();
+        User currentUser = userService.getCurrentUser();
+        String requestUri = request.getRequestURI();
+        String requestArgs = request.getQueryString();
+        String loginURI = userService.createLoginURL(requestUri + '?' + requestArgs);
+        String logoutURI = userService.createLogoutURL(loginURI);
+        if (currentUser == null || currentUser.getEmail() == null) {
+            response.sendRedirect(loginURI);
+            return;
+        }
+        PageType parentType = getNavParentType();
+        int activeIndex;
+        switch (getNavParentType()) {
+            case COVERAGE_OVERVIEW:
+                activeIndex = 2;
+                break;
+            case RELEASE:
+                activeIndex = 1;
+                break;
+            default:
+                activeIndex = 0;
+                break;
+        }
+        request.setAttribute("logoutURL", logoutURI);
+        request.setAttribute("email", currentUser.getEmail());
+        request.setAttribute("analyticsID", new Gson().toJson(ANALYTICS_ID));
+        request.setAttribute("breadcrumbLinks", getBreadcrumbLinks(request));
+        request.setAttribute("navbarLinks", navbarLinks);
+        request.setAttribute("activeIndex", activeIndex);
+        response.setContentType("text/html");
+        doGetHandler(request, response);
+    }
+
+    /**
+     * Implementation of the doGet method to be executed by servlet subclasses.
+     *
+     * @param request The HttpServletRequest object.
+     * @param response The HttpServletResponse object.
+     * @throws IOException
+     */
+    public abstract void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException;
+}
diff --git a/src/main/java/com/android/vts/servlet/DashboardMainServlet.java b/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
new file mode 100644
index 0000000..3766664
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/DashboardMainServlet.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.UserFavoriteEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PropertyProjection;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Represents the servlet that is invoked on loading the first page of dashboard. */
+public class DashboardMainServlet extends BaseServlet {
+    private static final String DASHBOARD_MAIN_JSP = "WEB-INF/jsp/dashboard_main.jsp";
+    private static final String DASHBOARD_ALL_LINK = "/?showAll=true";
+    private static final String DASHBOARD_FAVORITES_LINK = "/";
+    private static final String ALL_HEADER = "All Tests";
+    private static final String FAVORITES_HEADER = "Favorites";
+    private static final String NO_TESTS_ERROR = "No test results available.";
+    private static final String FAVORITES_BUTTON = "Show Favorites";
+    private static final String ALL_BUTTON = "Show All";
+    private static final String UP_ARROW = "keyboard_arrow_up";
+    private static final String DOWN_ARROW = "keyboard_arrow_down";
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.TOT;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        return null;
+    }
+
+    /** Helper class for displaying test entries on the main dashboard. */
+    public class TestDisplay implements Comparable<TestDisplay> {
+        private final Key testKey;
+        private final int passCount;
+        private final int failCount;
+
+        /**
+         * Test display constructor.
+         *
+         * @param testKey The key of the test.
+         * @param passCount The number of tests passing.
+         * @param failCount The number of tests failing.
+         */
+        public TestDisplay(Key testKey, int passCount, int failCount) {
+            this.testKey = testKey;
+            this.passCount = passCount;
+            this.failCount = failCount;
+        }
+
+        /**
+         * Get the key of the test.
+         *
+         * @return The key of the test.
+         */
+        public String getName() {
+            return this.testKey.getName();
+        }
+
+        /**
+         * Get the number of passing test cases.
+         *
+         * @return The number of passing test cases.
+         */
+        public int getPassCount() {
+            return this.passCount;
+        }
+
+        /**
+         * Get the number of failing test cases.
+         *
+         * @return The number of failing test cases.
+         */
+        public int getFailCount() {
+            return this.failCount;
+        }
+
+        @Override
+        public int compareTo(TestDisplay test) {
+            return this.testKey.getName().compareTo(test.getName());
+        }
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        UserService userService = UserServiceFactory.getUserService();
+        User currentUser = userService.getCurrentUser();
+        RequestDispatcher dispatcher = null;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+        List<TestDisplay> displayedTests = new ArrayList<>();
+        List<String> allTests = new ArrayList<>();
+
+        Map<Key, TestDisplay> testMap = new HashMap<>(); // map from table key to TestDisplay
+        Map<String, String> subscriptionMap = new HashMap<>();
+
+        boolean showAll = request.getParameter("showAll") != null;
+        String header;
+        String buttonLabel;
+        String buttonIcon;
+        String buttonLink;
+        String error = null;
+
+        Query q = new Query(TestEntity.KIND)
+                          .addProjection(new PropertyProjection(TestEntity.PASS_COUNT, Long.class))
+                          .addProjection(new PropertyProjection(TestEntity.FAIL_COUNT, Long.class));
+        for (Entity test : datastore.prepare(q).asIterable()) {
+            TestEntity testEntity = TestEntity.fromEntity(test);
+            if (test != null) {
+                TestDisplay display =
+                        new TestDisplay(test.getKey(), testEntity.passCount, testEntity.failCount);
+                testMap.put(test.getKey(), display);
+                allTests.add(test.getKey().getName());
+            }
+        }
+
+        if (testMap.size() == 0) {
+            error = NO_TESTS_ERROR;
+        }
+
+        if (showAll) {
+            for (Key testKey : testMap.keySet()) {
+                displayedTests.add(testMap.get(testKey));
+            }
+            header = ALL_HEADER;
+            buttonLabel = FAVORITES_BUTTON;
+            buttonIcon = UP_ARROW;
+            buttonLink = DASHBOARD_FAVORITES_LINK;
+        } else {
+            if (testMap.size() > 0) {
+                Filter userFilter = new FilterPredicate(
+                        UserFavoriteEntity.USER, FilterOperator.EQUAL, currentUser);
+                q = new Query(UserFavoriteEntity.KIND).setFilter(userFilter);
+
+                for (Entity favorite : datastore.prepare(q).asIterable()) {
+                    Key testKey = (Key) favorite.getProperty(UserFavoriteEntity.TEST_KEY);
+                    if (!testMap.containsKey(testKey)) {
+                        continue;
+                    }
+                    displayedTests.add(testMap.get(testKey));
+                    subscriptionMap.put(
+                            testKey.getName(), KeyFactory.keyToString(favorite.getKey()));
+                }
+            }
+            header = FAVORITES_HEADER;
+            buttonLabel = ALL_BUTTON;
+            buttonIcon = DOWN_ARROW;
+            buttonLink = DASHBOARD_ALL_LINK;
+        }
+        Collections.sort(displayedTests);
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        request.setAttribute("allTestsJson", new Gson().toJson(allTests));
+        request.setAttribute("subscriptionMapJson", new Gson().toJson(subscriptionMap));
+        request.setAttribute("testNames", displayedTests);
+        request.setAttribute("headerLabel", header);
+        request.setAttribute("showAll", showAll);
+        request.setAttribute("buttonLabel", buttonLabel);
+        request.setAttribute("buttonIcon", buttonIcon);
+        request.setAttribute("buttonLink", buttonLink);
+        request.setAttribute("error", error);
+        dispatcher = request.getRequestDispatcher(DASHBOARD_MAIN_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Excpetion caught : ", e);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowCoverageOverviewServlet.java b/src/main/java/com/android/vts/servlet/ShowCoverageOverviewServlet.java
new file mode 100644
index 0000000..dac864f
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowCoverageOverviewServlet.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage;
+import com.android.vts.util.TestRunMetadata;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.Query;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Represents the servlet that is invoked on loading the coverage overview page. */
+public class ShowCoverageOverviewServlet extends BaseServlet {
+    private static final String COVERAGE_OVERVIEW_JSP = "WEB-INF/jsp/show_coverage_overview.jsp";
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.COVERAGE_OVERVIEW;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        return null;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        RequestDispatcher dispatcher = null;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+        Query q = new Query(TestEntity.KIND).setKeysOnly();
+        List<Key> allTests = new ArrayList<>();
+        for (Entity test : datastore.prepare(q).asIterable()) {
+            allTests.add(test.getKey());
+        }
+
+        // Add test names to list
+        List<String> resultNames = new ArrayList<>();
+        for (VtsReportMessage.TestCaseResult r : VtsReportMessage.TestCaseResult.values()) {
+            resultNames.add(r.name());
+        }
+
+        List<JsonObject> testRunObjects = new ArrayList<>();
+
+        Query.Filter coverageFilter = new Query.FilterPredicate(
+                TestRunEntity.HAS_COVERAGE, Query.FilterOperator.EQUAL, true);
+        int coveredLines = 0;
+        int uncoveredLines = 0;
+        int passCount = 0;
+        int failCount = 0;
+        for (Key key : allTests) {
+            Query testRunQuery =
+                    new Query(TestRunEntity.KIND)
+                            .setAncestor(key)
+                            .setFilter(coverageFilter)
+                            .addSort(Entity.KEY_RESERVED_PROPERTY, Query.SortDirection.DESCENDING);
+            for (Entity testRunEntity :
+                    datastore.prepare(testRunQuery).asIterable(FetchOptions.Builder.withLimit(1))) {
+                TestRunEntity testRun = TestRunEntity.fromEntity(testRunEntity);
+                if (testRun == null)
+                    continue;
+                TestRunMetadata metadata = new TestRunMetadata(key.getName(), testRun);
+                testRunObjects.add(metadata.toJson());
+                coveredLines += testRun.coveredLineCount;
+                uncoveredLines += testRun.totalLineCount - testRun.coveredLineCount;
+                passCount += testRun.passCount;
+                failCount += testRun.failCount;
+            }
+        }
+
+        int[] testStats = new int[VtsReportMessage.TestCaseResult.values().length];
+        testStats[VtsReportMessage.TestCaseResult.TEST_CASE_RESULT_PASS.getNumber()] = passCount;
+        testStats[VtsReportMessage.TestCaseResult.TEST_CASE_RESULT_FAIL.getNumber()] = failCount;
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        request.setAttribute("resultNames", resultNames);
+        request.setAttribute("resultNamesJson", new Gson().toJson(resultNames));
+        request.setAttribute("testRuns", new Gson().toJson(testRunObjects));
+        request.setAttribute("coveredLines", new Gson().toJson(coveredLines));
+        request.setAttribute("uncoveredLines", new Gson().toJson(uncoveredLines));
+        request.setAttribute("testStats", new Gson().toJson(testStats));
+        dispatcher = request.getRequestDispatcher(COVERAGE_OVERVIEW_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Exception caught : ", e);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java b/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java
new file mode 100644
index 0000000..9efe7f8
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowCoverageServlet.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.CoverageEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Servlet for handling requests to show code coverage. */
+public class ShowCoverageServlet extends BaseServlet {
+    private static final String COVERAGE_JSP = "WEB-INF/jsp/show_coverage.jsp";
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.TOT;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String testName = request.getParameter("testName");
+        links.add(new Page(PageType.TABLE, testName, "?testName=" + testName));
+
+        String startTime = request.getParameter("startTime");
+        links.add(new Page(PageType.COVERAGE, "?testName=" + testName + "&startTime=" + startTime));
+        return links;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        RequestDispatcher dispatcher = null;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        String test = request.getParameter("testName");
+        String timeString = request.getParameter("startTime");
+
+        // Process the time key requested
+        long time = -1;
+        try {
+            time = Long.parseLong(timeString);
+        } catch (NumberFormatException e) {
+            request.setAttribute("testName", test);
+            dispatcher = request.getRequestDispatcher("/show_table.jsp");
+            return;
+        }
+
+        // Compute the parent test run key based off of the test and time
+        Key testKey = KeyFactory.createKey(TestEntity.KIND, test);
+        Key testRunKey = KeyFactory.createKey(testKey, TestRunEntity.KIND, time);
+
+        // Create a query for coverage entities
+        Query coverageQuery = new Query(CoverageEntity.KIND).setAncestor(testRunKey);
+
+        List<String> sourceFiles = new ArrayList<>(); // list of source files
+        List<List<Long>> coverageVectors = new ArrayList<>(); // list of line coverage vectors
+        List<String> projects = new ArrayList<>(); // list of project names
+        List<String> commits = new ArrayList<>(); // list of project commit hashes
+        List<String> indicators = new ArrayList<>(); // list of HTML indicates to display
+
+        /*
+         * Map from section name to a list of indexes into the above lists where each coverage
+         * report's data is located.
+         */
+        Map<String, List<Integer>> sectionMap = new HashMap<>();
+        for (Entity e : datastore.prepare(coverageQuery).asIterable()) {
+            CoverageEntity coverageEntity = CoverageEntity.fromEntity(e);
+            if (coverageEntity == null) {
+                logger.log(Level.WARNING, "Invalid coverage entity: " + e.getKey());
+                continue;
+            }
+            if (!sectionMap.containsKey(coverageEntity.group)) {
+                sectionMap.put(coverageEntity.group, new ArrayList<Integer>());
+            }
+            sectionMap.get(coverageEntity.group).add(coverageVectors.size());
+            coverageVectors.add(coverageEntity.lineCoverage);
+            sourceFiles.add(coverageEntity.filePath);
+            projects.add(coverageEntity.projectName);
+            commits.add(coverageEntity.projectVersion);
+            String indicator = "";
+            long total = coverageEntity.totalLineCount;
+            long covered = coverageEntity.coveredLineCount;
+            if (total > 0) {
+                double pct = Math.round(covered * 10000d / total) / 100d;
+                String color = pct >= 70 ? "green" : "red";
+                indicator = "<div class=\"right indicator " + color + "\">" + pct + "%</div>"
+                        + "<span class=\"right total-count\">" + covered + "/" + total + "</span>";
+            }
+            indicators.add(indicator);
+        }
+
+        request.setAttribute("testName", request.getParameter("testName"));
+        request.setAttribute("gerritURI", new Gson().toJson(GERRIT_URI));
+        request.setAttribute("gerritScope", new Gson().toJson(GERRIT_SCOPE));
+        request.setAttribute("clientId", new Gson().toJson(CLIENT_ID));
+        request.setAttribute("coverageVectors", new Gson().toJson(coverageVectors));
+        request.setAttribute("sourceFiles", new Gson().toJson(sourceFiles));
+        request.setAttribute("projects", new Gson().toJson(projects));
+        request.setAttribute("commits", new Gson().toJson(commits));
+        request.setAttribute("indicators", new Gson().toJson(indicators));
+        request.setAttribute("sectionMap", new Gson().toJson(sectionMap));
+        request.setAttribute("startTime", request.getParameter("startTime"));
+        dispatcher = request.getRequestDispatcher(COVERAGE_JSP);
+
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Exception caught : ", e);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowGraphServlet.java b/src/main/java/com/android/vts/servlet/ShowGraphServlet.java
new file mode 100644
index 0000000..8144ecf
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowGraphServlet.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.FilterUtil;
+import com.android.vts.util.Graph;
+import com.android.vts.util.GraphSerializer;
+import com.android.vts.util.Histogram;
+import com.android.vts.util.LineGraph;
+import com.android.vts.util.PerformanceUtil;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang.StringUtils;
+
+/** Servlet for handling requests to load graphs. */
+public class ShowGraphServlet extends BaseServlet {
+    private static final String GRAPH_JSP = "WEB-INF/jsp/show_graph.jsp";
+    private static final long DEFAULT_FILTER_OPTION = -1;
+
+    private static final String HIDL_HAL_OPTION = "hidl_hal_mode";
+    private static final String[] splitKeysArray = new String[] {HIDL_HAL_OPTION};
+    private static final Set<String> splitKeySet = new HashSet<>(Arrays.asList(splitKeysArray));
+    private static final String PROFILING_DATA_ALERT = "No profiling data was found.";
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.TOT;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String testName = request.getParameter("testName");
+        links.add(new Page(PageType.TABLE, testName, "?testName=" + testName));
+
+        String profilingPointName = request.getParameter("profilingPoint");
+        links.add(new Page(
+                PageType.GRAPH, "?testName=" + testName + "&profilingPoint=" + profilingPointName));
+        return links;
+    }
+
+    /**
+     * Process a profiling report message and add it to the map of graphs.
+     *
+     * @param profilingRun The Entity of a profiling point run to process.
+     * @param idString The ID derived from the test run to identify the profiling report.
+     * @param graphMap A map from graph name to Graph object.
+     */
+    private static void processProfilingRun(
+            Entity profilingRun, String idString, Map<String, Graph> graphMap) {
+        ProfilingPointRunEntity pt = ProfilingPointRunEntity.fromEntity(profilingRun);
+        if (pt == null)
+            return;
+        String name = PerformanceUtil.getOptionAlias(pt, splitKeySet);
+        Graph g = null;
+        if (pt.labels != null && pt.labels.size() == pt.values.size()) {
+            g = new LineGraph(name);
+        } else if (pt.labels == null && pt.values.size() > 0) {
+            g = new Histogram(name);
+        } else {
+            return;
+        }
+        if (!graphMap.containsKey(name)) {
+            graphMap.put(name, g);
+        }
+        graphMap.get(name).addData(idString, pt);
+    }
+
+    /**
+     * Get a summary string describing the devices in the test run.
+     *
+     * @param testRun The entity storing test run information.
+     * @param selectedDevice The name of the selected device.
+     * @return A string describing the devices in the test run, or null if it doesn't match filter.
+     */
+    private static String getDeviceSummary(Entity testRun, String selectedDevice) {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        List<String> buildInfos = new ArrayList<>();
+        Query deviceQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRun.getKey());
+        boolean isSelectedDevice = selectedDevice == null;
+        for (Entity device : datastore.prepare(deviceQuery).asIterable()) {
+            String product = (String) device.getProperty(DeviceInfoEntity.PRODUCT);
+            if (selectedDevice != null && product.equals(selectedDevice)) {
+                isSelectedDevice = true;
+            }
+            String buildId = (String) device.getProperty(DeviceInfoEntity.BUILD_ID);
+            buildInfos.add(product + " (" + buildId + ")");
+        }
+        return isSelectedDevice ? StringUtils.join(buildInfos, ", ") : null;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        RequestDispatcher dispatcher = null;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        String testName = request.getParameter("testName");
+        String profilingPointName = request.getParameter("profilingPoint");
+        String selectedDevice = request.getParameter("device");
+        Long endTime = null;
+        if (request.getParameter("endTime") != null) {
+            String time = request.getParameter("endTime");
+            try {
+                endTime = Long.parseLong(time);
+            } catch (NumberFormatException e) {
+            }
+        }
+        if (endTime == null) {
+            endTime = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+        }
+        Long startTime = endTime - TimeUnit.DAYS.toMicros(1);
+
+        // Set of device names
+        List<String> devices = DatastoreHelper.getAllProducts();
+        if (!devices.contains(selectedDevice))
+            selectedDevice = null;
+
+        Map<String, Graph> graphMap = new HashMap<>();
+
+        // Create a query for test runs matching the time window filter
+        Key parentKey = KeyFactory.createKey(TestEntity.KIND, testName);
+        Filter timeFilter =
+                FilterUtil.getTimeFilter(parentKey, TestRunEntity.KIND, startTime, endTime);
+        Query testRunQuery = new Query(TestRunEntity.KIND)
+                                     .setAncestor(parentKey)
+                                     .setFilter(timeFilter)
+                                     .setKeysOnly();
+
+        // Process the test runs in the query
+        for (Entity testRun : datastore.prepare(testRunQuery).asIterable()) {
+            String buildInfoString = getDeviceSummary(testRun, selectedDevice);
+            if (buildInfoString == null) {
+                continue;
+            }
+
+            try {
+                Entity profilingRun = datastore.get(KeyFactory.createKey(
+                        testRun.getKey(), ProfilingPointRunEntity.KIND, profilingPointName));
+                processProfilingRun(profilingRun, buildInfoString, graphMap);
+            } catch (EntityNotFoundException e) {
+                // Profiling point not collected during this test run
+                continue;
+            }
+        }
+        // Get the names of the graphs to render
+        String[] names = graphMap.keySet().toArray(new String[graphMap.size()]);
+        Arrays.sort(names);
+
+        List<Graph> graphList = new ArrayList<>();
+        boolean hasHistogram = false;
+        for (String name : names) {
+            Graph g = graphMap.get(name);
+            if (g.size() > 0) {
+                graphList.add(g);
+                if (g instanceof Histogram)
+                    hasHistogram = true;
+            }
+        }
+
+        String filterVal = request.getParameter("filterVal");
+        try {
+            Long.parseLong(filterVal);
+        } catch (NumberFormatException e) {
+            filterVal = Long.toString(DEFAULT_FILTER_OPTION);
+        }
+        request.setAttribute("testName", request.getParameter("testName"));
+        request.setAttribute("filterVal", filterVal);
+        request.setAttribute("endTime", new Gson().toJson(endTime));
+        request.setAttribute("devices", devices);
+        request.setAttribute("selectedDevice", selectedDevice);
+        request.setAttribute("showFilterDropdown", hasHistogram);
+        if (graphList.size() == 0)
+            request.setAttribute("error", PROFILING_DATA_ALERT);
+
+        Gson gson = new GsonBuilder()
+                            .registerTypeHierarchyAdapter(Graph.class, new GraphSerializer())
+                            .create();
+        request.setAttribute("graphs", gson.toJson(graphList));
+
+        request.setAttribute("profilingPointName", profilingPointName);
+        dispatcher = request.getRequestDispatcher(GRAPH_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Excpetion caught : ", e);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java b/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java
new file mode 100644
index 0000000..51b0127
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowPerformanceDigestServlet.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.PerformanceSummary;
+import com.android.vts.util.PerformanceUtil;
+import com.android.vts.util.PerformanceUtil.TimeInterval;
+import com.android.vts.util.ProfilingPointSummary;
+import com.android.vts.util.StatSummary;
+import java.io.IOException;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Servlet for producing tabular performance summaries. */
+public class ShowPerformanceDigestServlet extends BaseServlet {
+    private static final String PERF_DIGEST_JSP = "WEB-INF/jsp/show_performance_digest.jsp";
+    private static final String HIDL_HAL_OPTION = "hidl_hal_mode";
+    private static final String[] splitKeysArray = new String[] {HIDL_HAL_OPTION};
+    private static final Set<String> splitKeySet =
+            new HashSet<String>(Arrays.asList(splitKeysArray));
+
+    private static final String MEAN = "Mean";
+    private static final String MIN = "Min";
+    private static final String MAX = "Max";
+    private static final String MEAN_DELTA = "&Delta;Mean (%)";
+    private static final String HIGHER_IS_BETTER =
+            "Note: Higher values are better. Maximum is the best-case performance.";
+    private static final String LOWER_IS_BETTER =
+            "Note: Lower values are better. Minimum is the best-case performance.";
+    private static final String STD = "Std";
+
+    private static final DecimalFormat FORMATTER;
+
+    /**
+     * Initialize the decimal formatter.
+     */
+    static {
+        FORMATTER = new DecimalFormat("#.##");
+        FORMATTER.setRoundingMode(RoundingMode.HALF_UP);
+    }
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.TOT;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String testName = request.getParameter("testName");
+        links.add(new Page(PageType.TABLE, testName, "?testName=" + testName));
+        links.add(new Page(PageType.PERFORMANCE, "?testName=" + testName));
+        return links;
+    }
+
+    /**
+     * Generates an HTML summary of the performance changes for the profiling results in the
+     * specified
+     * table.
+     *
+     * <p>Retrieves the past 24 hours of profiling data and compares it to the 24 hours that
+     * preceded
+     * it. Creates a table representation of the mean and standard deviation for each profiling
+     * point.
+     * When performance degrades, the cell is shaded red.
+     *
+     * @param profilingPoint The name of the profiling point to summarize.
+     * @param testSummary The ProfilingPointSummary object to compare against.
+     * @param perfSummaries List of PerformanceSummary objects for each profiling run (in reverse
+     *     chronological order).
+     * @param sectionLabels HTML string for the section labels (i.e. for each time interval).
+     * @returns An HTML string for a table comparing the profiling point results across time
+     *     intervals.
+     */
+    public static String getPeformanceSummary(String profilingPoint,
+            ProfilingPointSummary testSummary, List<PerformanceSummary> perfSummaries,
+            String sectionLabels) {
+        String tableHTML = "<table>";
+
+        // Format section labels
+        tableHTML += "<tr>";
+        tableHTML += "<th class='section-label grey lighten-2'>";
+        tableHTML += testSummary.yLabel + "</th>";
+        tableHTML += sectionLabels;
+        tableHTML += "</tr>";
+
+        String bestCaseString;
+        switch (testSummary.getRegressionMode()) {
+            case VTS_REGRESSION_MODE_DECREASING:
+                bestCaseString = MAX;
+                break;
+            default:
+                bestCaseString = MIN;
+                break;
+        }
+
+        // Format column labels
+        tableHTML += "<tr>";
+        for (int i = 0; i <= perfSummaries.size() + 1; i++) {
+            if (i > 1) {
+                tableHTML += "<th class='section-label grey lighten-2'>" + MEAN_DELTA + "</th>";
+            }
+            if (i == 0) {
+                tableHTML += "<th class='section-label grey lighten-2'>";
+                tableHTML += testSummary.xLabel + "</th>";
+            } else if (i > 0) {
+                tableHTML += "<th class='section-label grey lighten-2'>" + bestCaseString + "</th>";
+                tableHTML += "<th class='section-label grey lighten-2'>" + MEAN + "</th>";
+                tableHTML += "<th class='section-label grey lighten-2'>" + STD + "</th>";
+            }
+        }
+        tableHTML += "</tr>";
+
+        // Populate data cells
+        for (StatSummary stats : testSummary) {
+            String label = stats.getLabel();
+            tableHTML += "<tr><td class='axis-label grey lighten-2'>" + label;
+            tableHTML += "</td><td class='cell inner-cell'>";
+            tableHTML += FORMATTER.format(stats.getBestCase()) + "</td>";
+            tableHTML += "<td class='cell inner-cell'>";
+            tableHTML += FORMATTER.format(stats.getMean()) + "</td>";
+            tableHTML += "<td class='cell outer-cell'>";
+            tableHTML += FORMATTER.format(stats.getStd()) + "</td>";
+            for (PerformanceSummary prevPerformance : perfSummaries) {
+                if (prevPerformance.hasProfilingPoint(profilingPoint)) {
+                    StatSummary baseline = prevPerformance.getProfilingPointSummary(profilingPoint)
+                                                   .getStatSummary(label);
+                    tableHTML += PerformanceUtil.getAvgCasePerformanceComparisonHTML(
+                            baseline, stats, "cell inner-cell", "cell outer-cell", "", "");
+                } else {
+                    tableHTML += "<td></td><td></td><td></td><td></td>";
+                }
+            }
+            tableHTML += "</tr>";
+        }
+        tableHTML += "</table>";
+        return tableHTML;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        RequestDispatcher dispatcher = null;
+        String testName = request.getParameter("testName");
+        String selectedDevice = request.getParameter("device");
+        Long startTime = null;
+        if (request.getParameter("startTime") != null) {
+            String time = request.getParameter("startTime");
+            try {
+                startTime = Long.parseLong(time);
+            } catch (NumberFormatException e) {
+                logger.log(Level.WARNING, "Invalid start time passed to digest servlet: " + time);
+            }
+        }
+        if (startTime == null) {
+            startTime = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+        }
+
+        // Add today to the list of time intervals to analyze
+        List<TimeInterval> timeIntervals = new ArrayList<>();
+        TimeInterval today = new TimeInterval(startTime - TimeUnit.DAYS.toMicros(1), startTime);
+        timeIntervals.add(today);
+
+        // Add yesterday as a baseline time interval for analysis
+        long oneDayAgo = startTime - TimeUnit.DAYS.toMicros(1);
+        TimeInterval yesterday = new TimeInterval(oneDayAgo - TimeUnit.DAYS.toMicros(1), oneDayAgo);
+        timeIntervals.add(yesterday);
+
+        // Add last week as a baseline time interval for analysis
+        long oneWeek = TimeUnit.DAYS.toMicros(7);
+        long oneWeekAgo = startTime - oneWeek;
+        String spanString = "<span class='date-label'>";
+        String label =
+                spanString + TimeUnit.MICROSECONDS.toMillis(oneWeekAgo - oneWeek) + "</span>";
+        label += " - " + spanString + TimeUnit.MICROSECONDS.toMillis(oneWeekAgo) + "</span>";
+        TimeInterval lastWeek = new TimeInterval(oneWeekAgo - oneWeek, oneWeekAgo, label);
+        timeIntervals.add(lastWeek);
+
+        List<PerformanceSummary> perfSummaries = new ArrayList<>();
+
+        String sectionLabels = "";
+        int i = 0;
+        for (TimeInterval interval : timeIntervals) {
+            PerformanceSummary perfSummary = new PerformanceSummary(splitKeySet);
+            PerformanceUtil.updatePerformanceSummary(
+                    testName, interval.start, interval.end, selectedDevice, perfSummary);
+            if (perfSummary.size() == 0) {
+                continue;
+            }
+            perfSummaries.add(perfSummary);
+            String content = interval.label;
+            sectionLabels += "<th class='section-label grey lighten-2 center' ";
+            if (i++ == 0)
+                sectionLabels += "colspan='3'";
+            else
+                sectionLabels += "colspan='4'";
+            sectionLabels += ">" + content + "</th>";
+        }
+
+        List<String> tables = new ArrayList<>();
+        List<String> tableTitles = new ArrayList<>();
+        List<String> tableSubtitles = new ArrayList<>();
+        if (perfSummaries.size() != 0) {
+            PerformanceSummary todayPerformance = perfSummaries.remove(0);
+            String[] profilingNames = todayPerformance.getProfilingPointNames();
+
+            for (String profilingPointName : profilingNames) {
+                ProfilingPointSummary baselinePerformance =
+                        todayPerformance.getProfilingPointSummary(profilingPointName);
+                String table = getPeformanceSummary(
+                        profilingPointName, baselinePerformance, perfSummaries, sectionLabels);
+                if (table != null) {
+                    tables.add(table);
+                    tableTitles.add(profilingPointName);
+                    switch (baselinePerformance.getRegressionMode()) {
+                        case VTS_REGRESSION_MODE_DECREASING:
+                            tableSubtitles.add(HIGHER_IS_BETTER);
+                            break;
+                        default:
+                            tableSubtitles.add(LOWER_IS_BETTER);
+                            break;
+                    }
+                }
+            }
+        }
+
+        request.setAttribute("testName", testName);
+        request.setAttribute("tables", tables);
+        request.setAttribute("tableTitles", tableTitles);
+        request.setAttribute("tableSubtitles", tableSubtitles);
+        request.setAttribute("startTime", Long.toString(startTime));
+        request.setAttribute("selectedDevice", selectedDevice);
+        request.setAttribute("devices", DatastoreHelper.getAllProducts());
+
+        dispatcher = request.getRequestDispatcher(PERF_DIGEST_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Exception caught : " + e.toString());
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowPlanReleaseServlet.java b/src/main/java/com/android/vts/servlet/ShowPlanReleaseServlet.java
new file mode 100644
index 0000000..4e88bcc
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowPlanReleaseServlet.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.TestPlanEntity;
+import com.android.vts.entity.TestPlanRunEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.FilterUtil;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.Set;
+import java.util.HashSet;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang.StringUtils;
+
+public class ShowPlanReleaseServlet extends BaseServlet {
+    private static final String PLAN_RELEASE_JSP = "WEB-INF/jsp/show_plan_release.jsp";
+    private static final int MAX_RUNS_PER_PAGE = 90;
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.RELEASE;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String planName = request.getParameter("plan");
+        links.add(new Page(PageType.PLAN_RELEASE, planName.toUpperCase(), "?plan=" + planName));
+        return links;
+    }
+
+    /**
+     * Model to describe each test plan run .
+     */
+    private class TestPlanRunMetadata implements Comparable<TestPlanRunMetadata> {
+        public final TestPlanRunEntity testPlanRun;
+        public final List<String> devices;
+        public final Set<DeviceInfoEntity> deviceSet;
+
+        public TestPlanRunMetadata(TestPlanRunEntity testPlanRun) {
+            this.testPlanRun = testPlanRun;
+            this.devices = new ArrayList<>();
+            this.deviceSet = new HashSet<>();
+        }
+
+        public void addDevice(DeviceInfoEntity device) {
+            if (device == null || deviceSet.contains(device))
+                return;
+            devices.add(device.branch + "/" + device.buildFlavor + " (" + device.buildId + ")");
+            deviceSet.add(device);
+        }
+
+        public JsonObject toJson() {
+            JsonObject obj = new JsonObject();
+            obj.add("testPlanRun", testPlanRun.toJson());
+            obj.add("deviceInfo", new JsonPrimitive(StringUtils.join(devices, ", ")));
+            return obj;
+        }
+
+        @Override
+        public int compareTo(TestPlanRunMetadata o) {
+            return new Long(o.testPlanRun.startTimestamp)
+                    .compareTo(this.testPlanRun.startTimestamp);
+        }
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Long startTime = null; // time in microseconds
+        Long endTime = null; // time in microseconds
+        if (request.getParameter("startTime") != null) {
+            String time = request.getParameter("startTime");
+            try {
+                startTime = Long.parseLong(time);
+                startTime = startTime > 0 ? startTime : null;
+            } catch (NumberFormatException e) {
+                startTime = null;
+            }
+        }
+        if (request.getParameter("endTime") != null) {
+            String time = request.getParameter("endTime");
+            try {
+                endTime = Long.parseLong(time);
+                endTime = endTime > 0 ? endTime : null;
+            } catch (NumberFormatException e) {
+                endTime = null;
+            }
+        }
+        SortDirection dir = SortDirection.DESCENDING;
+        if (startTime != null && endTime == null) {
+            dir = SortDirection.ASCENDING;
+        }
+        boolean unfiltered = request.getParameter("unfiltered") != null;
+        boolean showPresubmit = request.getParameter("showPresubmit") != null;
+        boolean showPostsubmit = request.getParameter("showPostsubmit") != null;
+        // If no params are specified, set to default of postsubmit-only.
+        if (!(showPresubmit || showPostsubmit)) {
+            showPostsubmit = true;
+        }
+
+        // If unfiltered, set showPre- and Post-submit to true for accurate UI.
+        if (unfiltered) {
+            showPostsubmit = true;
+            showPresubmit = true;
+        }
+        Filter typeFilter = FilterUtil.getTestTypeFilter(showPresubmit, showPostsubmit, unfiltered);
+        String testPlan = request.getParameter("plan");
+        Key testPlanKey = KeyFactory.createKey(TestPlanEntity.KIND, testPlan);
+        Filter testPlanRunFilter = FilterUtil.getTimeFilter(
+                testPlanKey, TestPlanRunEntity.KIND, startTime, endTime, typeFilter);
+        Map<String, Object> parameterMap = request.getParameterMap();
+        Filter userDeviceFilter = FilterUtil.getUserDeviceFilter(parameterMap);
+
+        List<TestPlanRunMetadata> testPlanRuns = new ArrayList<>();
+        if (userDeviceFilter == null) {
+            Query testPlanRunQuery = new Query(TestPlanRunEntity.KIND)
+                                             .setAncestor(testPlanKey)
+                                             .setFilter(testPlanRunFilter)
+                                             .addSort(Entity.KEY_RESERVED_PROPERTY, dir);
+            for (Entity testPlanRunEntity :
+                    datastore.prepare(testPlanRunQuery)
+                            .asIterable(FetchOptions.Builder.withLimit(MAX_RUNS_PER_PAGE))) {
+                TestPlanRunEntity testPlanRun = TestPlanRunEntity.fromEntity(testPlanRunEntity);
+                if (testPlanRun == null) {
+                    logger.log(
+                            Level.WARNING, "Invalid test plan run: " + testPlanRunEntity.getKey());
+                    continue;
+                }
+                TestPlanRunMetadata metadata = new TestPlanRunMetadata(testPlanRun);
+                testPlanRuns.add(metadata);
+                Query deviceInfoQuery =
+                        new Query(DeviceInfoEntity.KIND).setAncestor(testPlanRun.key);
+                for (Entity deviceInfoEntity : datastore.prepare(deviceInfoQuery).asIterable()) {
+                    DeviceInfoEntity deviceInfo = DeviceInfoEntity.fromEntity(deviceInfoEntity);
+                    metadata.addDevice(deviceInfo);
+                }
+            }
+        } else {
+            List<Key> gets = FilterUtil.getMatchingKeys(testPlanKey, TestPlanRunEntity.KIND,
+                    testPlanRunFilter, userDeviceFilter, dir, MAX_RUNS_PER_PAGE);
+            Map<Key, Entity> entityMap = datastore.get(gets);
+            for (Key key : gets) {
+                if (!entityMap.containsKey(key)) {
+                    continue;
+                }
+                TestPlanRunEntity testPlanRun = TestPlanRunEntity.fromEntity(entityMap.get(key));
+                if (testPlanRun == null) {
+                    continue;
+                }
+                TestPlanRunMetadata metadata = new TestPlanRunMetadata(testPlanRun);
+                testPlanRuns.add(metadata);
+                Query deviceInfoQuery =
+                        new Query(DeviceInfoEntity.KIND).setAncestor(testPlanRun.key);
+                for (Entity deviceInfoEntity : datastore.prepare(deviceInfoQuery).asIterable()) {
+                    DeviceInfoEntity deviceInfo = DeviceInfoEntity.fromEntity(deviceInfoEntity);
+                    metadata.addDevice(deviceInfo);
+                }
+            }
+        }
+
+        Collections.sort(testPlanRuns);
+
+        if (testPlanRuns.size() > 0) {
+            TestPlanRunMetadata firstRun = testPlanRuns.get(0);
+            endTime = firstRun.testPlanRun.startTimestamp;
+
+            TestPlanRunMetadata lastRun = testPlanRuns.get(testPlanRuns.size() - 1);
+            startTime = lastRun.testPlanRun.startTimestamp;
+        }
+
+        List<JsonObject> testPlanRunObjects = new ArrayList<>();
+        for (TestPlanRunMetadata metadata : testPlanRuns) {
+            testPlanRunObjects.add(metadata.toJson());
+        }
+
+        FilterUtil.setAttributes(request, parameterMap);
+
+        request.setAttribute("plan", request.getParameter("plan"));
+        request.setAttribute("hasNewer", new Gson().toJson(DatastoreHelper.hasNewer(
+                                                 testPlanKey, TestPlanRunEntity.KIND, endTime)));
+        request.setAttribute("hasOlder", new Gson().toJson(DatastoreHelper.hasOlder(
+                                                 testPlanKey, TestPlanRunEntity.KIND, startTime)));
+        request.setAttribute("planRuns", new Gson().toJson(testPlanRunObjects));
+
+        request.setAttribute("unfiltered", unfiltered);
+        request.setAttribute("showPresubmit", showPresubmit);
+        request.setAttribute("showPostsubmit", showPostsubmit);
+        request.setAttribute("startTime", new Gson().toJson(startTime));
+        request.setAttribute("endTime", new Gson().toJson(endTime));
+        request.setAttribute("branches", new Gson().toJson(DatastoreHelper.getAllBranches()));
+        request.setAttribute("devices", new Gson().toJson(DatastoreHelper.getAllBuildFlavors()));
+        response.setStatus(HttpServletResponse.SC_OK);
+        RequestDispatcher dispatcher = request.getRequestDispatcher(PLAN_RELEASE_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Excpetion caught : ", e);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowPlanRunServlet.java b/src/main/java/com/android/vts/servlet/ShowPlanRunServlet.java
new file mode 100644
index 0000000..32b3ec4
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowPlanRunServlet.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.TestPlanEntity;
+import com.android.vts.entity.TestPlanRunEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.android.vts.util.TestRunMetadata;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Servlet for handling requests to load individual plan runs. */
+public class ShowPlanRunServlet extends BaseServlet {
+    private static final String PLAN_RUN_JSP = "WEB-INF/jsp/show_plan_run.jsp";
+    private static final String PROFILING_DATA_ALERT = "No profiling data was found.";
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.RELEASE;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String planName = request.getParameter("plan");
+        links.add(new Page(PageType.PLAN_RELEASE, planName.toUpperCase(), "?plan=" + planName));
+
+        String time = request.getParameter("time");
+        links.add(new Page(PageType.PLAN_RUN, "?plan=" + planName + "&time=" + time));
+        return links;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        Long startTime = null; // time in microseconds
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        RequestDispatcher dispatcher = null;
+
+        String plan = request.getParameter("plan");
+
+        if (request.getParameter("time") != null) {
+            String time = request.getParameter("time");
+            try {
+                startTime = Long.parseLong(time);
+                startTime = startTime > 0 ? startTime : null;
+            } catch (NumberFormatException e) {
+                startTime = null;
+            }
+        }
+
+        // Add result names to list
+        List<String> resultNames = new ArrayList<>();
+        for (TestCaseResult r : TestCaseResult.values()) {
+            resultNames.add(r.name());
+        }
+
+        List<TestRunMetadata> testRunMetadata = new ArrayList<>();
+        List<JsonObject> testRunObjects = new ArrayList<>();
+
+        Key planKey = KeyFactory.createKey(TestPlanEntity.KIND, plan);
+        Key planRunKey = KeyFactory.createKey(planKey, TestPlanRunEntity.KIND, startTime);
+        int passCount = 0;
+        int failCount = 0;
+        try {
+            Entity testPlanRunEntity = datastore.get(planRunKey);
+            TestPlanRunEntity testPlanRun = TestPlanRunEntity.fromEntity(testPlanRunEntity);
+            Map<Key, Entity> testRuns = datastore.get(testPlanRun.testRuns);
+            passCount = (int) testPlanRun.passCount;
+            failCount = (int) testPlanRun.failCount;
+
+            for (Key key : testPlanRun.testRuns) {
+                if (!testRuns.containsKey(key))
+                    continue;
+                TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRuns.get(key));
+                if (testRunEntity == null)
+                    continue;
+                TestRunMetadata metadata =
+                        new TestRunMetadata(key.getParent().getName(), testRunEntity);
+                testRunMetadata.add(metadata);
+                testRunObjects.add(metadata.toJson());
+            }
+        } catch (EntityNotFoundException e) {
+            // Invalid parameters
+        }
+
+        int[] topBuildResultCounts = new int[TestCaseResult.values().length];
+        topBuildResultCounts = new int[TestCaseResult.values().length];
+        topBuildResultCounts[TestCaseResult.TEST_CASE_RESULT_PASS.getNumber()] = passCount;
+        topBuildResultCounts[TestCaseResult.TEST_CASE_RESULT_FAIL.getNumber()] = failCount;
+
+        Set<String> profilingPoints = new HashSet<>();
+
+        String profilingDataAlert = "";
+        if (profilingPoints.size() == 0) {
+            profilingDataAlert = PROFILING_DATA_ALERT;
+        }
+        List<String> profilingPointNames = new ArrayList<>(profilingPoints);
+        Collections.sort(profilingPointNames);
+
+        request.setAttribute("plan", request.getParameter("plan"));
+        request.setAttribute("time", request.getParameter("time"));
+
+        request.setAttribute("error", profilingDataAlert);
+
+        request.setAttribute("profilingPointNames", profilingPointNames);
+        request.setAttribute("resultNames", resultNames);
+        request.setAttribute("resultNamesJson", new Gson().toJson(resultNames));
+        request.setAttribute("testRuns", new Gson().toJson(testRunObjects));
+
+        // data for pie chart
+        request.setAttribute("topBuildResultCounts", new Gson().toJson(topBuildResultCounts));
+
+        dispatcher = request.getRequestDispatcher(PLAN_RUN_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Exception caught : " + e.toString());
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowReleaseServlet.java b/src/main/java/com/android/vts/servlet/ShowReleaseServlet.java
new file mode 100644
index 0000000..35f1af2
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowReleaseServlet.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestPlanEntity;
+import com.android.vts.entity.UserFavoriteEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PropertyProjection;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.users.User;
+import com.google.appengine.api.users.UserService;
+import com.google.appengine.api.users.UserServiceFactory;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Represents the servlet that is invoked on loading the release page. */
+public class ShowReleaseServlet extends BaseServlet {
+    private static final String RELEASE_JSP = "WEB-INF/jsp/show_release.jsp";
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.RELEASE;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        return null;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        UserService userService = UserServiceFactory.getUserService();
+        User currentUser = userService.getCurrentUser();
+        RequestDispatcher dispatcher = null;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+        Set<String> planSet = new HashSet<>();
+
+        Query q = new Query(TestPlanEntity.KIND).setKeysOnly();
+        for (Entity testPlanEntity : datastore.prepare(q).asIterable()) {
+            planSet.add(testPlanEntity.getKey().getName());
+        }
+
+        List<String> plans = new ArrayList<>(planSet);
+        Collections.sort(plans);
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        request.setAttribute("planNames", plans);
+        dispatcher = request.getRequestDispatcher(RELEASE_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Excpetion caught : ", e);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowTableServlet.java b/src/main/java/com/android/vts/servlet/ShowTableServlet.java
new file mode 100644
index 0000000..b525c3d
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowTableServlet.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.FilterUtil;
+import com.android.vts.util.TestResults;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang.StringUtils;
+
+/** Servlet for handling requests to load individual tables. */
+public class ShowTableServlet extends BaseServlet {
+    private static final String TABLE_JSP = "WEB-INF/jsp/show_table.jsp";
+    // Error message displayed on the webpage is tableName passed is null.
+    private static final String TABLE_NAME_ERROR = "Error : Table name must be passed!";
+    private static final String PROFILING_DATA_ALERT = "No profiling data was found.";
+    private static final int MAX_BUILD_IDS_PER_PAGE = 10;
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.TOT;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String testName = request.getParameter("testName");
+        links.add(new Page(PageType.TABLE, testName, "?testName=" + testName));
+        return links;
+    }
+
+    public static void processTestRun(TestResults testResults, Entity testRun) {
+        TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRun);
+        if (testRunEntity == null) {
+            return;
+        }
+
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        List<Key> gets = new ArrayList<>();
+        for (long testCaseId : testRunEntity.testCaseIds) {
+            gets.add(KeyFactory.createKey(TestCaseRunEntity.KIND, testCaseId));
+        }
+
+        List<Entity> testCases = new ArrayList<>();
+        Map<Key, Entity> entityMap = datastore.get(gets);
+        for (Key key : gets) {
+            if (entityMap.containsKey(key)) {
+                testCases.add(entityMap.get(key));
+            }
+        }
+
+        Query deviceInfoQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRun.getKey());
+        Iterable<Entity> deviceInfos = datastore.prepare(deviceInfoQuery).asIterable();
+
+        Query profilingPointQuery =
+                new Query(ProfilingPointRunEntity.KIND).setAncestor(testRun.getKey()).setKeysOnly();
+        Iterable<Entity> profilingPoints = datastore.prepare(profilingPointQuery).asIterable();
+
+        testResults.addTestRun(testRun, testCases, deviceInfos, profilingPoints);
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        boolean unfiltered = request.getParameter("unfiltered") != null;
+        boolean showPresubmit = request.getParameter("showPresubmit") != null;
+        boolean showPostsubmit = request.getParameter("showPostsubmit") != null;
+        String searchString = request.getParameter("search");
+        Long startTime = null; // time in microseconds
+        Long endTime = null; // time in microseconds
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        RequestDispatcher dispatcher = null;
+
+        // message to display if profiling point data is not available
+        String profilingDataAlert = "";
+
+        if (request.getParameter("testName") == null) {
+            request.setAttribute("testName", TABLE_NAME_ERROR);
+            return;
+        }
+        String testName = request.getParameter("testName");
+
+        if (request.getParameter("startTime") != null) {
+            String time = request.getParameter("startTime");
+            try {
+                startTime = Long.parseLong(time);
+                startTime = startTime > 0 ? startTime : null;
+            } catch (NumberFormatException e) {
+                startTime = null;
+            }
+        }
+        if (request.getParameter("endTime") != null) {
+            String time = request.getParameter("endTime");
+            try {
+                endTime = Long.parseLong(time);
+                endTime = endTime > 0 ? endTime : null;
+            } catch (NumberFormatException e) {
+                endTime = null;
+            }
+        }
+
+        // If no params are specified, set to default of postsubmit-only.
+        if (!(showPresubmit || showPostsubmit)) {
+            showPostsubmit = true;
+        }
+
+        // If unfiltered, set showPre- and Post-submit to true for accurate UI.
+        if (unfiltered) {
+            showPostsubmit = true;
+            showPresubmit = true;
+        }
+
+        // Add result names to list
+        List<String> resultNames = new ArrayList<>();
+        for (TestCaseResult r : TestCaseResult.values()) {
+            resultNames.add(r.name());
+        }
+
+        TestResults testResults = new TestResults(testName);
+
+        SortDirection dir = SortDirection.DESCENDING;
+        if (startTime != null && endTime == null) {
+            dir = SortDirection.ASCENDING;
+        }
+        Key testKey = KeyFactory.createKey(TestEntity.KIND, testName);
+        Map<String, Object> parameterMap = request.getParameterMap();
+        Filter userTestFilter = FilterUtil.getUserTestFilter(parameterMap);
+        Filter userDeviceFilter = FilterUtil.getUserDeviceFilter(parameterMap);
+
+        Filter typeFilter = FilterUtil.getTestTypeFilter(showPresubmit, showPostsubmit, unfiltered);
+        Filter testFilter = FilterUtil.getTimeFilter(
+                testKey, TestRunEntity.KIND, startTime, endTime, typeFilter);
+        if (userTestFilter == null && userDeviceFilter == null) {
+            Query testRunQuery = new Query(TestRunEntity.KIND)
+                                         .setAncestor(testKey)
+                                         .setFilter(testFilter)
+                                         .addSort(Entity.KEY_RESERVED_PROPERTY, dir);
+            for (Entity testRun :
+                    datastore.prepare(testRunQuery)
+                            .asIterable(FetchOptions.Builder.withLimit(MAX_BUILD_IDS_PER_PAGE))) {
+                processTestRun(testResults, testRun);
+            }
+        } else {
+            if (userTestFilter != null) {
+                testFilter = CompositeFilterOperator.and(userTestFilter, testFilter);
+            }
+            List<Key> gets = FilterUtil.getMatchingKeys(testKey, TestRunEntity.KIND, testFilter,
+                    userDeviceFilter, dir, MAX_BUILD_IDS_PER_PAGE);
+            Map<Key, Entity> entityMap = datastore.get(gets);
+            for (Key key : gets) {
+                if (!entityMap.containsKey(key)) {
+                    continue;
+                }
+                processTestRun(testResults, entityMap.get(key));
+            }
+        }
+
+        testResults.processReport();
+
+        if (testResults.profilingPointNames.length == 0) {
+            profilingDataAlert = PROFILING_DATA_ALERT;
+        }
+
+        FilterUtil.setAttributes(request, parameterMap);
+
+        request.setAttribute("testName", request.getParameter("testName"));
+
+        request.setAttribute("error", profilingDataAlert);
+
+        // pass values by converting to JSON
+        request.setAttribute("headerRow", new Gson().toJson(testResults.headerRow));
+        request.setAttribute("timeGrid", new Gson().toJson(testResults.timeGrid));
+        request.setAttribute("durationGrid", new Gson().toJson(testResults.durationGrid));
+        request.setAttribute("summaryGrid", new Gson().toJson(testResults.summaryGrid));
+        request.setAttribute("resultsGrid", new Gson().toJson(testResults.resultsGrid));
+        request.setAttribute("profilingPointNames", testResults.profilingPointNames);
+        request.setAttribute("resultNames", resultNames);
+        request.setAttribute("resultNamesJson", new Gson().toJson(resultNames));
+        request.setAttribute("logInfoMap", new Gson().toJson(testResults.logInfoMap));
+
+        // data for pie chart
+        request.setAttribute(
+                "topBuildResultCounts", new Gson().toJson(testResults.totResultCounts));
+        request.setAttribute("topBuildId", testResults.totBuildId);
+        request.setAttribute("startTime", new Gson().toJson(testResults.startTime));
+        request.setAttribute("endTime", new Gson().toJson(testResults.endTime));
+        request.setAttribute("hasNewer", new Gson().toJson(DatastoreHelper.hasNewer(testKey,
+                                                 TestRunEntity.KIND, testResults.endTime)));
+        request.setAttribute("hasOlder", new Gson().toJson(DatastoreHelper.hasOlder(testKey,
+                                                 TestRunEntity.KIND, testResults.startTime)));
+        request.setAttribute("unfiltered", unfiltered);
+        request.setAttribute("showPresubmit", showPresubmit);
+        request.setAttribute("showPostsubmit", showPostsubmit);
+
+        request.setAttribute("branches", new Gson().toJson(DatastoreHelper.getAllBranches()));
+        request.setAttribute("devices", new Gson().toJson(DatastoreHelper.getAllBuildFlavors()));
+
+        dispatcher = request.getRequestDispatcher(TABLE_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Exception caught : " + e.toString());
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/ShowTreeServlet.java b/src/main/java/com/android/vts/servlet/ShowTreeServlet.java
new file mode 100644
index 0000000..d6e759e
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/ShowTreeServlet.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.FilterUtil;
+import com.android.vts.util.TestRunDetails;
+import com.android.vts.util.TestRunMetadata;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang.StringUtils;
+
+/** Servlet for handling requests to load individual tables. */
+public class ShowTreeServlet extends BaseServlet {
+    private static final String TABLE_JSP = "WEB-INF/jsp/show_tree.jsp";
+    // Error message displayed on the webpage is tableName passed is null.
+    private static final String TABLE_NAME_ERROR = "Error : Table name must be passed!";
+    private static final String PROFILING_DATA_ALERT = "No profiling data was found.";
+    private static final int MAX_BUILD_IDS_PER_PAGE = 20;
+    private static final int MAX_PREFETCH_COUNT = 10;
+
+    @Override
+    public PageType getNavParentType() {
+        return PageType.TOT;
+    }
+
+    @Override
+    public List<Page> getBreadcrumbLinks(HttpServletRequest request) {
+        List<Page> links = new ArrayList<>();
+        String testName = request.getParameter("testName");
+        links.add(new Page(PageType.TREE, testName, "?testName=" + testName));
+        return links;
+    }
+
+    /**
+     * Get the test run details for a test run.
+     * @param metadata The metadata for the test run whose details will be fetched.
+     * @return The TestRunDetails object for the provided test run.
+     */
+    public static TestRunDetails processTestDetails(TestRunMetadata metadata) {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        TestRunDetails details = new TestRunDetails();
+        List<Key> gets = new ArrayList<>();
+        for (long testCaseId : metadata.testRun.testCaseIds) {
+            gets.add(KeyFactory.createKey(TestCaseRunEntity.KIND, testCaseId));
+        }
+        Map<Key, Entity> entityMap = datastore.get(gets);
+        for (int i = 0; i < 1; i++) {
+            for (Key key : entityMap.keySet()) {
+                TestCaseRunEntity testCaseRun = TestCaseRunEntity.fromEntity(entityMap.get(key));
+                if (testCaseRun == null) {
+                    continue;
+                }
+                details.addTestCase(testCaseRun);
+            }
+        }
+        return details;
+    }
+
+    @Override
+    public void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException {
+        boolean unfiltered = request.getParameter("unfiltered") != null;
+        boolean showPresubmit = request.getParameter("showPresubmit") != null;
+        boolean showPostsubmit = request.getParameter("showPostsubmit") != null;
+        String searchString = request.getParameter("search");
+        Long startTime = null; // time in microseconds
+        Long endTime = null; // time in microseconds
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        RequestDispatcher dispatcher = null;
+
+        // message to display if profiling point data is not available
+        String profilingDataAlert = "";
+
+        if (request.getParameter("testName") == null) {
+            request.setAttribute("testName", TABLE_NAME_ERROR);
+            return;
+        }
+        String testName = request.getParameter("testName");
+
+        if (request.getParameter("startTime") != null) {
+            String time = request.getParameter("startTime");
+            try {
+                startTime = Long.parseLong(time);
+                startTime = startTime > 0 ? startTime : null;
+            } catch (NumberFormatException e) {
+                startTime = null;
+            }
+        }
+        if (request.getParameter("endTime") != null) {
+            String time = request.getParameter("endTime");
+            try {
+                endTime = Long.parseLong(time);
+                endTime = endTime > 0 ? endTime : null;
+            } catch (NumberFormatException e) {
+                endTime = null;
+            }
+        }
+
+        // If no params are specified, set to default of postsubmit-only.
+        if (!(showPresubmit || showPostsubmit)) {
+            showPostsubmit = true;
+        }
+
+        // If unfiltered, set showPre- and Post-submit to true for accurate UI.
+        if (unfiltered) {
+            showPostsubmit = true;
+            showPresubmit = true;
+        }
+
+        // Add result names to list
+        List<String> resultNames = new ArrayList<>();
+        for (TestCaseResult r : TestCaseResult.values()) {
+            resultNames.add(r.name());
+        }
+
+        SortDirection dir = SortDirection.DESCENDING;
+        if (startTime != null && endTime == null) {
+            dir = SortDirection.ASCENDING;
+        }
+        Key testKey = KeyFactory.createKey(TestEntity.KIND, testName);
+        Map<String, Object> parameterMap = request.getParameterMap();
+        Filter userTestFilter = FilterUtil.getUserTestFilter(parameterMap);
+        Filter userDeviceFilter = FilterUtil.getUserDeviceFilter(parameterMap);
+
+        Filter typeFilter = FilterUtil.getTestTypeFilter(showPresubmit, showPostsubmit, unfiltered);
+        Filter testFilter = FilterUtil.getTimeFilter(
+                testKey, TestRunEntity.KIND, startTime, endTime, typeFilter);
+
+        List<TestRunMetadata> testRunMetadata = new ArrayList<>();
+        if (userTestFilter == null && userDeviceFilter == null) {
+            Query testRunQuery = new Query(TestRunEntity.KIND)
+                                         .setAncestor(testKey)
+                                         .setFilter(testFilter)
+                                         .addSort(Entity.KEY_RESERVED_PROPERTY, dir);
+            for (Entity testRun :
+                    datastore.prepare(testRunQuery)
+                            .asIterable(FetchOptions.Builder.withLimit(MAX_BUILD_IDS_PER_PAGE))) {
+                TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRun);
+                if (testRunEntity == null) {
+                    continue;
+                }
+                TestRunMetadata metadata = new TestRunMetadata(testName, testRunEntity);
+                testRunMetadata.add(metadata);
+            }
+        } else {
+            if (userTestFilter != null) {
+                testFilter = Query.CompositeFilterOperator.and(userTestFilter, testFilter);
+            }
+            List<Key> gets = FilterUtil.getMatchingKeys(testKey, TestRunEntity.KIND, testFilter,
+                    userDeviceFilter, dir, MAX_BUILD_IDS_PER_PAGE);
+            Map<Key, Entity> entityMap = datastore.get(gets);
+            for (Key key : gets) {
+                if (!entityMap.containsKey(key)) {
+                    continue;
+                }
+                TestRunEntity testRunEntity = TestRunEntity.fromEntity(entityMap.get(key));
+                if (testRunEntity == null) {
+                    continue;
+                }
+                TestRunMetadata metadata = new TestRunMetadata(testName, testRunEntity);
+                testRunMetadata.add(metadata);
+            }
+        }
+
+        Comparator<TestRunMetadata> comparator = new Comparator<TestRunMetadata>() {
+            @Override
+            public int compare(TestRunMetadata t1, TestRunMetadata t2) {
+                return new Long(t2.testRun.startTimestamp).compareTo(t1.testRun.startTimestamp);
+            }
+        };
+        Collections.sort(testRunMetadata, comparator);
+        List<JsonObject> testRunObjects = new ArrayList<>();
+
+        int prefetchCount = 0;
+        for (TestRunMetadata metadata : testRunMetadata) {
+            if (metadata.testRun.failCount > 0 && prefetchCount < MAX_PREFETCH_COUNT) {
+                // process
+                metadata.addDetails(processTestDetails(metadata));
+                ++prefetchCount;
+            }
+            testRunObjects.add(metadata.toJson());
+        }
+
+        int[] topBuildResultCounts = null;
+        String topBuild = "";
+        if (testRunMetadata.size() > 0) {
+            TestRunMetadata firstRun = testRunMetadata.get(0);
+            topBuild = firstRun.getDeviceInfo();
+            endTime = firstRun.testRun.startTimestamp;
+            TestRunDetails topDetails = firstRun.getDetails();
+            if (topDetails == null) {
+                topDetails = processTestDetails(firstRun);
+            }
+            topBuildResultCounts = topDetails.resultCounts;
+
+            TestRunMetadata lastRun = testRunMetadata.get(testRunMetadata.size() - 1);
+            startTime = lastRun.testRun.startTimestamp;
+        }
+
+        Set<String> profilingPoints = new HashSet<>();
+        Query profilingPointQuery =
+                new Query(ProfilingPointRunEntity.KIND).setAncestor(testKey).setKeysOnly();
+        for (Entity e : datastore.prepare(profilingPointQuery).asIterable()) {
+            profilingPoints.add(e.getKey().getName());
+        }
+
+        if (profilingPoints.size() == 0) {
+            profilingDataAlert = PROFILING_DATA_ALERT;
+        }
+        List<String> profilingPointNames = new ArrayList<>(profilingPoints);
+        Collections.sort(profilingPointNames);
+
+        FilterUtil.setAttributes(request, parameterMap);
+
+        request.setAttribute("testName", request.getParameter("testName"));
+
+        request.setAttribute("error", profilingDataAlert);
+
+        request.setAttribute("profilingPointNames", profilingPointNames);
+        request.setAttribute("resultNames", resultNames);
+        request.setAttribute("resultNamesJson", new Gson().toJson(resultNames));
+        request.setAttribute("testRuns", new Gson().toJson(testRunObjects));
+
+        // data for pie chart
+        request.setAttribute("topBuildResultCounts", new Gson().toJson(topBuildResultCounts));
+        request.setAttribute("topBuildId", topBuild);
+        request.setAttribute("startTime", new Gson().toJson(startTime));
+        request.setAttribute("endTime", new Gson().toJson(endTime));
+        request.setAttribute("hasNewer",
+                new Gson().toJson(DatastoreHelper.hasNewer(testKey, TestRunEntity.KIND, endTime)));
+        request.setAttribute("hasOlder", new Gson().toJson(DatastoreHelper.hasOlder(
+                                                 testKey, TestRunEntity.KIND, startTime)));
+        request.setAttribute("unfiltered", unfiltered);
+        request.setAttribute("showPresubmit", showPresubmit);
+        request.setAttribute("showPostsubmit", showPostsubmit);
+
+        request.setAttribute("branches", new Gson().toJson(DatastoreHelper.getAllBranches()));
+        request.setAttribute("devices", new Gson().toJson(DatastoreHelper.getAllBuildFlavors()));
+
+        dispatcher = request.getRequestDispatcher(TABLE_JSP);
+        try {
+            dispatcher.forward(request, response);
+        } catch (ServletException e) {
+            logger.log(Level.SEVERE, "Servlet Exception caught : " + e.toString());
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java b/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java
new file mode 100644
index 0000000..9e48108
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/VtsAlertJobServlet.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestCaseRunEntity.TestCase;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestEntity.TestCaseReference;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.android.vts.util.DatastoreHelper;
+import com.android.vts.util.EmailHelper;
+import com.android.vts.util.FilterUtil;
+import com.google.appengine.api.datastore.DatastoreFailureException;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.DatastoreTimeoutException;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.SortDirection;
+import com.google.appengine.api.datastore.Transaction;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.math3.analysis.function.Add;
+
+/** Represents the notifications service which is automatically called on a fixed schedule. */
+public class VtsAlertJobServlet extends HttpServlet {
+    protected final Logger logger = Logger.getLogger(getClass().getName());
+
+    /**
+     * Creates an email footer with the provided link.
+     *
+     * @param link The (string) link to provide in the footer.
+     * @return The full HTML email footer.
+     */
+    private String getFooter(String link) {
+        return "<br><br>For details, visit the"
+                + " <a href='" + link + "'>"
+                + "VTS dashboard.</a>";
+    }
+
+    /**
+     * Compose an email if the test is inactive.
+     *
+     * @param test The TestEntity document storing the test status.
+     * @param link Fully specified link to the test's status page.
+     * @param emails The list of email addresses to send the email.
+     * @param messages The message list in which to insert the inactivity notification email.
+     * @return True if the test is inactive, false otherwise.
+     */
+    private boolean notifyIfInactive(
+            TestEntity test, String link, List<String> emails, List<Message> messages) {
+        long now = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+        long diff = now - test.timestamp;
+        // Send an email daily to notify that the test hasn't been running.
+        // After 7 full days have passed, notifications will no longer be sent (i.e. the
+        // test is assumed to be deprecated).
+        if (diff > TimeUnit.DAYS.toMicros(1) && diff < TimeUnit.DAYS.toMicros(8)
+                && diff % TimeUnit.DAYS.toMicros(1) < TimeUnit.MINUTES.toMicros(3)) {
+            Date lastUpload = new Date(TimeUnit.MICROSECONDS.toMillis(test.timestamp));
+            String uploadTimeString =
+                    new SimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(lastUpload);
+            String subject = "Warning! Inactive test: " + test.testName;
+            String body = "Hello,<br><br>Test \"" + test.testName + "\" is inactive. "
+                    + "No new data has been uploaded since " + uploadTimeString + "."
+                    + getFooter(link);
+            try {
+                messages.add(EmailHelper.composeEmail(emails, subject, body));
+                return true;
+            } catch (MessagingException | UnsupportedEncodingException e) {
+                logger.log(Level.WARNING, "Error composing email : ", e);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether any new failures have occurred beginning since (and including) startTime.
+     *
+     * @param test The TestEntity object for the test.
+     * @param link The string URL linking to the test's status table.
+     * @param failedTestCaseMap The map of test case names to TestCase for those failing in the
+     *     last status update.
+     * @param emailAddresses The list of email addresses to send notifications to.
+     * @param messages The email Message queue.
+     * @returns latest TestStatusMessage or null if no update is available.
+     * @throws IOException
+     */
+    public TestEntity getTestStatus(TestEntity test, String link,
+            Map<String, TestCase> failedTestCaseMap, List<String> emailAddresses,
+            List<Message> messages) throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        String footer = getFooter(link);
+
+        TestRunEntity mostRecentRun = null;
+        Map<String, TestCaseResult> mostRecentTestCaseResults = new HashMap<>();
+        Map<String, TestCase> testCaseBreakageMap = new HashMap<>();
+        int passingTestcaseCount = 0;
+        List<TestCaseReference> failingTestCases = new ArrayList<>();
+        Set<String> fixedTestcases = new HashSet<>();
+        Set<String> newTestcaseFailures = new HashSet<>();
+        Set<String> continuedTestcaseFailures = new HashSet<>();
+        Set<String> skippedTestcaseFailures = new HashSet<>();
+        Set<String> transientTestcaseFailures = new HashSet<>();
+
+        String testName = test.testName;
+        Key testKey = KeyFactory.createKey(TestEntity.KIND, testName);
+        Filter testTypeFilter = FilterUtil.getTestTypeFilter(false, true, false);
+        Filter runFilter = FilterUtil.getTimeFilter(
+                testKey, TestRunEntity.KIND, test.timestamp + 1, null, testTypeFilter);
+        Query q = new Query(TestRunEntity.KIND)
+                          .setAncestor(testKey)
+                          .setFilter(runFilter)
+                          .addSort(Entity.KEY_RESERVED_PROPERTY, SortDirection.DESCENDING);
+
+        for (Entity testRun : datastore.prepare(q).asIterable()) {
+            TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRun);
+            if (testRunEntity == null) {
+                logger.log(Level.WARNING, "Invalid test run detected: " + testRun.getKey());
+            }
+            if (mostRecentRun == null) {
+                mostRecentRun = testRunEntity;
+            }
+            List<Key> testCaseKeys = new ArrayList<>();
+            for (long testCaseId : testRunEntity.testCaseIds) {
+                testCaseKeys.add(KeyFactory.createKey(TestCaseRunEntity.KIND, testCaseId));
+            }
+            Map<Key, Entity> entityMap = datastore.get(testCaseKeys);
+            for (Key testCaseKey : testCaseKeys) {
+                if (!entityMap.containsKey(testCaseKey)) {
+                    logger.log(Level.WARNING, "Test case entity missing: " + testCaseKey);
+                    continue;
+                }
+                Entity testCaseRun = entityMap.get(testCaseKey);
+                TestCaseRunEntity testCaseRunEntity = TestCaseRunEntity.fromEntity(testCaseRun);
+                if (testCaseRunEntity == null) {
+                    logger.log(Level.WARNING, "Invalid test case run: " + testCaseRun.getKey());
+                    continue;
+                }
+                for (TestCase testCase : testCaseRunEntity.testCases) {
+                    String testCaseName = testCase.name;
+                    TestCaseResult result = TestCaseResult.valueOf(testCase.result);
+
+                    if (mostRecentRun == testRunEntity) {
+                        mostRecentTestCaseResults.put(testCaseName, result);
+                    } else {
+                        if (!mostRecentTestCaseResults.containsKey(testCaseName)) {
+                            // Deprecate notifications for tests that are not present on newer runs
+                            continue;
+                        }
+                        TestCaseResult mostRecentRes = mostRecentTestCaseResults.get(testCaseName);
+                        if (mostRecentRes == TestCaseResult.TEST_CASE_RESULT_SKIP) {
+                            mostRecentTestCaseResults.put(testCaseName, result);
+                        } else if (mostRecentRes == TestCaseResult.TEST_CASE_RESULT_PASS) {
+                            // Test is passing now, witnessed a transient failure
+                            if (result != TestCaseResult.TEST_CASE_RESULT_PASS
+                                    && result != TestCaseResult.TEST_CASE_RESULT_SKIP) {
+                                transientTestcaseFailures.add(testCaseName);
+                            }
+                        }
+                    }
+
+                    // Record test case breakages
+                    if (result != TestCaseResult.TEST_CASE_RESULT_PASS
+                            && result != TestCaseResult.TEST_CASE_RESULT_SKIP) {
+                        testCaseBreakageMap.put(testCaseName, testCase);
+                    }
+                }
+            }
+        }
+
+        if (mostRecentRun == null) {
+            notifyIfInactive(test, link, emailAddresses, messages);
+            return null;
+        }
+
+        for (String testCaseName : mostRecentTestCaseResults.keySet()) {
+            TestCaseResult mostRecentResult = mostRecentTestCaseResults.get(testCaseName);
+            boolean previouslyFailed = failedTestCaseMap.containsKey(testCaseName);
+            if (mostRecentResult == TestCaseResult.TEST_CASE_RESULT_SKIP) {
+                // persist previous status
+                if (previouslyFailed) {
+                    failingTestCases.add(
+                            new TestCaseReference(failedTestCaseMap.get(testCaseName)));
+                } else {
+                    ++passingTestcaseCount;
+                }
+            } else if (mostRecentResult == TestCaseResult.TEST_CASE_RESULT_PASS) {
+                ++passingTestcaseCount;
+                if (previouslyFailed && !transientTestcaseFailures.contains(testCaseName)) {
+                    fixedTestcases.add(testCaseName);
+                }
+            } else {
+                if (!previouslyFailed) {
+                    newTestcaseFailures.add(testCaseName);
+                    failingTestCases.add(
+                            new TestCaseReference(testCaseBreakageMap.get(testCaseName)));
+                } else {
+                    continuedTestcaseFailures.add(testCaseName);
+                    failingTestCases.add(
+                            new TestCaseReference(failedTestCaseMap.get(testCaseName)));
+                }
+            }
+        }
+
+        Set<String> buildIdList = new HashSet<>();
+        Query deviceQuery = new Query(DeviceInfoEntity.KIND).setAncestor(mostRecentRun.key);
+        for (Entity device : datastore.prepare(deviceQuery).asIterable()) {
+            DeviceInfoEntity deviceEntity = DeviceInfoEntity.fromEntity(device);
+            if (deviceEntity == null) {
+                continue;
+            }
+            buildIdList.add(deviceEntity.buildId);
+        }
+        String buildId = StringUtils.join(buildIdList, ",");
+        String summary = new String();
+        if (newTestcaseFailures.size() + continuedTestcaseFailures.size() > 0) {
+            summary += "The following test cases failed in the latest test run:<br>";
+
+            // Add new test case failures to top of summary in bold font.
+            List<String> sortedNewTestcaseFailures = new ArrayList<>(newTestcaseFailures);
+            Collections.sort(sortedNewTestcaseFailures);
+            for (String testcaseName : sortedNewTestcaseFailures) {
+                summary += "- "
+                        + "<b>" + testcaseName + "</b><br>";
+            }
+
+            // Add continued test case failures to summary.
+            List<String> sortedContinuedTestcaseFailures =
+                    new ArrayList<>(continuedTestcaseFailures);
+            Collections.sort(sortedContinuedTestcaseFailures);
+            for (String testcaseName : sortedContinuedTestcaseFailures) {
+                summary += "- " + testcaseName + "<br>";
+            }
+        }
+        if (fixedTestcases.size() > 0) {
+            // Add fixed test cases to summary.
+            summary += "<br><br>The following test cases were fixed in the latest test run:<br>";
+            List<String> sortedFixedTestcases = new ArrayList<>(fixedTestcases);
+            Collections.sort(sortedFixedTestcases);
+            for (String testcaseName : sortedFixedTestcases) {
+                summary += "- <i>" + testcaseName + "</i><br>";
+            }
+        }
+        if (transientTestcaseFailures.size() > 0) {
+            // Add transient test case failures to summary.
+            summary += "<br><br>The following transient test case failures occured:<br>";
+            List<String> sortedTransientTestcaseFailures =
+                    new ArrayList<>(transientTestcaseFailures);
+            Collections.sort(sortedTransientTestcaseFailures);
+            for (String testcaseName : sortedTransientTestcaseFailures) {
+                summary += "- " + testcaseName + "<br>";
+            }
+        }
+        if (skippedTestcaseFailures.size() > 0) {
+            // Add skipped test case failures to summary.
+            summary += "<br><br>The following test cases have not been run since failing:<br>";
+            List<String> sortedSkippedTestcaseFailures = new ArrayList<>(skippedTestcaseFailures);
+            Collections.sort(sortedSkippedTestcaseFailures);
+            for (String testcaseName : sortedSkippedTestcaseFailures) {
+                summary += "- " + testcaseName + "<br>";
+            }
+        }
+
+        if (newTestcaseFailures.size() > 0) {
+            String subject = "New test failures in " + testName + " @ " + buildId;
+            String body = "Hello,<br><br>Test cases are failing in " + testName
+                    + " for device build ID(s): " + buildId + ".<br><br>" + summary + footer;
+            try {
+                messages.add(EmailHelper.composeEmail(emailAddresses, subject, body));
+            } catch (MessagingException | UnsupportedEncodingException e) {
+                logger.log(Level.WARNING, "Error composing email : ", e);
+            }
+        } else if (continuedTestcaseFailures.size() > 0) {
+            String subject = "Continued test failures in " + testName + " @ " + buildId;
+            String body = "Hello,<br><br>Test cases are failing in " + testName
+                    + " for device build ID(s): " + buildId + ".<br><br>" + summary + footer;
+            try {
+                messages.add(EmailHelper.composeEmail(emailAddresses, subject, body));
+            } catch (MessagingException | UnsupportedEncodingException e) {
+                logger.log(Level.WARNING, "Error composing email : ", e);
+            }
+        } else if (transientTestcaseFailures.size() > 0) {
+            String subject = "Transient test failure in " + testName + " @ " + buildId;
+            String body = "Hello,<br><br>Some test cases failed in " + testName + " but tests all "
+                    + "are passing in the latest device build(s): " + buildId + ".<br><br>"
+                    + summary + footer;
+            try {
+                messages.add(EmailHelper.composeEmail(emailAddresses, subject, body));
+            } catch (MessagingException | UnsupportedEncodingException e) {
+                logger.log(Level.WARNING, "Error composing email : ", e);
+            }
+        } else if (fixedTestcases.size() > 0) {
+            String subject = "All test cases passing in " + testName + " @ " + buildId;
+            String body = "Hello,<br><br>All test cases passed in " + testName
+                    + " for device build ID(s): " + buildId + "!<br><br>" + summary + footer;
+            try {
+                messages.add(EmailHelper.composeEmail(emailAddresses, subject, body));
+            } catch (MessagingException | UnsupportedEncodingException e) {
+                logger.log(Level.WARNING, "Error composing email : ", e);
+            }
+        }
+        return new TestEntity(test.testName, mostRecentRun.startTimestamp, passingTestcaseCount,
+                failingTestCases.size(), failingTestCases);
+    }
+
+    /**
+     * Process the current test case failures for a test.
+     *
+     * @param testEntity The TestEntity object for the test.
+     * @returns a map from test case name to the test case run ID for which the test case failed.
+     */
+    public static Map<String, TestCase> getCurrentFailures(TestEntity testEntity) {
+        if (testEntity.failingTestCases == null || testEntity.failingTestCases.size() == 0) {
+            return new HashMap<>();
+        }
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Map<String, TestCase> failingTestcases = new HashMap<>();
+        Set<Key> gets = new HashSet<>();
+        for (TestCaseReference testCaseRef : testEntity.failingTestCases) {
+            gets.add(KeyFactory.createKey(TestCaseRunEntity.KIND, testCaseRef.parentId));
+        }
+        if (gets.size() == 0) {
+            return failingTestcases;
+        }
+        Map<Key, Entity> testCaseMap = datastore.get(gets);
+
+        for (TestCaseReference testCaseRef : testEntity.failingTestCases) {
+            Key key = KeyFactory.createKey(TestCaseRunEntity.KIND, testCaseRef.parentId);
+            if (!testCaseMap.containsKey(key)) {
+                continue;
+            }
+            Entity testCaseRun = testCaseMap.get(key);
+            TestCaseRunEntity testCaseRunEntity = TestCaseRunEntity.fromEntity(testCaseRun);
+            if (testCaseRunEntity.testCases.size() <= testCaseRef.offset) {
+                continue;
+            }
+            TestCase testCase = testCaseRunEntity.testCases.get(testCaseRef.offset);
+            failingTestcases.put(testCase.name, testCase);
+        }
+        return failingTestcases;
+    }
+
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Query q = new Query(TestEntity.KIND);
+        for (Entity test : datastore.prepare(q).asIterable()) {
+            TestEntity testEntity = TestEntity.fromEntity(test);
+            if (testEntity == null) {
+                logger.log(Level.WARNING, "Corrupted test entity: " + test.getKey().getName());
+                continue;
+            }
+            List<String> emails = EmailHelper.getSubscriberEmails(test.getKey());
+
+            StringBuffer fullUrl = request.getRequestURL();
+            String baseUrl = fullUrl.substring(0, fullUrl.indexOf(request.getRequestURI()));
+            String link = baseUrl + "/show_table?testName=" + testEntity.testName;
+
+            List<Message> messageQueue = new ArrayList<>();
+            Map<String, TestCase> failedTestcaseMap = getCurrentFailures(testEntity);
+
+            TestEntity newTestEntity =
+                    getTestStatus(testEntity, link, failedTestcaseMap, emails, messageQueue);
+
+            // Send any inactivity notifications
+            if (newTestEntity == null) {
+                if (messageQueue.size() > 0) {
+                    EmailHelper.sendAll(messageQueue);
+                }
+                continue;
+            }
+
+            int retries = 0;
+            while (true) {
+                Transaction txn = datastore.beginTransaction();
+                try {
+                    try {
+                        testEntity = TestEntity.fromEntity(datastore.get(test.getKey()));
+
+                        // Another job updated the test entity
+                        if (testEntity == null || testEntity.timestamp >= newTestEntity.timestamp) {
+                            txn.rollback();
+                        } else { // This update is most recent.
+                            datastore.put(newTestEntity.toEntity());
+                            txn.commit();
+                            EmailHelper.sendAll(messageQueue);
+                        }
+                    } catch (EntityNotFoundException e) {
+                        logger.log(Level.INFO,
+                                "Test disappeared during updated: " + newTestEntity.testName);
+                    }
+                    break;
+                } catch (ConcurrentModificationException | DatastoreFailureException
+                        | DatastoreTimeoutException e) {
+                    logger.log(Level.WARNING, "Retrying alert job insert: " + test.getKey());
+                    if (retries++ >= DatastoreHelper.MAX_WRITE_RETRIES) {
+                        logger.log(Level.SEVERE, "Exceeded alert job retries: " + test.getKey());
+                        throw e;
+                    }
+                } finally {
+                    if (txn.isActive()) {
+                        txn.rollback();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java b/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java
new file mode 100644
index 0000000..616f5c7
--- /dev/null
+++ b/src/main/java/com/android/vts/servlet/VtsPerformanceJobServlet.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import com.android.vts.entity.TestEntity;
+import com.android.vts.util.EmailHelper;
+import com.android.vts.util.PerformanceSummary;
+import com.android.vts.util.PerformanceUtil;
+import com.android.vts.util.PerformanceUtil.TimeInterval;
+import com.android.vts.util.ProfilingPointSummary;
+import com.android.vts.util.StatSummary;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.Query;
+import java.io.IOException;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Represents the notifications service which is automatically called on a fixed schedule. */
+public class VtsPerformanceJobServlet extends HttpServlet {
+    private static final String MEAN = "Mean";
+    private static final String MAX = "Max";
+    private static final String MIN = "Min";
+    private static final String MIN_DELTA = "&Delta;Min (%)";
+    private static final String MAX_DELTA = "&Delta;Max (%)";
+    private static final String HIGHER_IS_BETTER =
+            "Note: Higher values are better. Maximum is the best-case performance.";
+    private static final String LOWER_IS_BETTER =
+            "Note: Lower values are better. Minimum is the best-case performance.";
+    private static final String STD = "Std";
+    private static final String SUBJECT_PREFIX = "Daily Performance Digest: ";
+    private static final String LAST_WEEK = "Last Week";
+    private static final String LABEL_STYLE = "font-family: arial";
+    private static final String SUBTEXT_STYLE = "font-family: arial; font-size: 12px";
+    private static final String TABLE_STYLE =
+            "width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;";
+    private static final String SECTION_LABEL_STYLE =
+            "border: 1px solid black; border-bottom: none; background-color: lightgray;";
+    private static final String COL_LABEL_STYLE =
+            "border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;";
+    private static final String HEADER_COL_STYLE =
+            "border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;";
+    private static final String INNER_CELL_STYLE =
+            "border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;";
+    private static final String OUTER_CELL_STYLE =
+            "border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;";
+
+    private static final DecimalFormat FORMATTER;
+
+    /**
+     * Initialize the decimal formatter.
+     */
+    static {
+        FORMATTER = new DecimalFormat("#.##");
+        FORMATTER.setRoundingMode(RoundingMode.HALF_UP);
+    }
+
+    /**
+     * Generates an HTML summary of the performance changes for the profiling results in the
+     * specified
+     * table.
+     *
+     * <p>Retrieves the past 24 hours of profiling data and compares it to the 24 hours that
+     * preceded
+     * it. Creates a table representation of the mean and standard deviation for each profiling
+     * point.
+     * When performance degrades, the cell is shaded red.
+     *
+     * @param testName The name of the test whose profiling data to summarize.
+     * @param perfSummaries List of PerformanceSummary objects for each profiling run (in reverse
+     *     chronological order).
+     * @param labels List of string labels for use as the column headers.
+     * @returns An HTML string containing labeled table summaries.
+     */
+    public static String getPeformanceSummary(
+            String testName, List<PerformanceSummary> perfSummaries, List<String> labels) {
+        if (perfSummaries.size() == 0)
+            return "";
+        PerformanceSummary now = perfSummaries.get(0);
+        String tableHTML = "<p style='" + LABEL_STYLE + "'><b>";
+        tableHTML += testName + "</b></p>";
+        for (String profilingPoint : now.getProfilingPointNames()) {
+            ProfilingPointSummary summary = now.getProfilingPointSummary(profilingPoint);
+            tableHTML += "<table cellpadding='2' style='" + TABLE_STYLE + "'>";
+
+            // Format header rows
+            String[] headerRows = new String[] {profilingPoint, summary.yLabel};
+            int colspan = labels.size() * 4;
+            for (String content : headerRows) {
+                tableHTML += "<tr><td colspan='" + colspan + "'>" + content + "</td></tr>";
+            }
+
+            // Format section labels
+            tableHTML += "<tr>";
+            for (int i = 0; i < labels.size(); i++) {
+                String content = labels.get(i);
+                tableHTML += "<th style='" + SECTION_LABEL_STYLE + "' ";
+                if (i == 0)
+                    tableHTML += "colspan='1'";
+                else if (i == 1)
+                    tableHTML += "colspan='3'";
+                else
+                    tableHTML += "colspan='4'";
+                tableHTML += ">" + content + "</th>";
+            }
+            tableHTML += "</tr>";
+
+            String deltaString;
+            String bestCaseString;
+            String subtext;
+            switch (now.getProfilingPointSummary(profilingPoint).getRegressionMode()) {
+                case VTS_REGRESSION_MODE_DECREASING:
+                    deltaString = MAX_DELTA;
+                    bestCaseString = MAX;
+                    subtext = HIGHER_IS_BETTER;
+                    break;
+                default:
+                    deltaString = MIN_DELTA;
+                    bestCaseString = MIN;
+                    subtext = LOWER_IS_BETTER;
+                    break;
+            }
+
+            // Format column labels
+            tableHTML += "<tr>";
+            for (int i = 0; i < labels.size(); i++) {
+                if (i > 1) {
+                    tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + deltaString + "</th>";
+                }
+                if (i == 0) {
+                    tableHTML += "<th style='" + COL_LABEL_STYLE + "'>";
+                    tableHTML += summary.xLabel + "</th>";
+                } else if (i > 0) {
+                    tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + bestCaseString + "</th>";
+                    tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + MEAN + "</th>";
+                    tableHTML += "<th style='" + COL_LABEL_STYLE + "'>" + STD + "</th>";
+                }
+            }
+            tableHTML += "</tr>";
+
+            // Populate data cells
+            for (StatSummary stats : summary) {
+                String label = stats.getLabel();
+                tableHTML += "<tr><td style='" + HEADER_COL_STYLE + "'>" + label;
+                tableHTML += "</td><td style='" + INNER_CELL_STYLE + "'>";
+                tableHTML += FORMATTER.format(stats.getBestCase()) + "</td>";
+                tableHTML += "<td style='" + INNER_CELL_STYLE + "'>";
+                tableHTML += FORMATTER.format(stats.getMean()) + "</td>";
+                tableHTML += "<td style='" + OUTER_CELL_STYLE + "'>";
+                tableHTML += FORMATTER.format(stats.getStd()) + "</td>";
+                for (int i = 1; i < perfSummaries.size(); i++) {
+                    PerformanceSummary oldPerfSummary = perfSummaries.get(i);
+                    if (oldPerfSummary.hasProfilingPoint(profilingPoint)) {
+                        StatSummary baseline =
+                                oldPerfSummary.getProfilingPointSummary(profilingPoint)
+                                        .getStatSummary(label);
+                        tableHTML += PerformanceUtil.getBestCasePerformanceComparisonHTML(
+                                baseline, stats, "", "", INNER_CELL_STYLE, OUTER_CELL_STYLE);
+                    } else
+                        tableHTML += "<td></td><td></td><td></td><td></td>";
+                }
+                tableHTML += "</tr>";
+            }
+            tableHTML += "</table>";
+            tableHTML += "<i style='" + SUBTEXT_STYLE + "'>" + subtext + "</i><br><br>";
+        }
+        return tableHTML;
+    }
+
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Set<Key> allTestKeys = new HashSet<>();
+
+        Query q = new Query(TestEntity.KIND).setKeysOnly();
+        for (Entity test : datastore.prepare(q).asIterable()) {
+            if (test.getKey().getName() == null) {
+                continue;
+            }
+            allTestKeys.add(test.getKey());
+        }
+
+        // Add today to the list of time intervals to analyze
+        List<TimeInterval> timeIntervals = new ArrayList<>();
+        long nowMilli = System.currentTimeMillis();
+        long nowMicro = TimeUnit.MILLISECONDS.toMicros(nowMilli);
+        String dateString = new SimpleDateFormat("MM-dd-yyyy").format(new Date(nowMilli));
+        TimeInterval today =
+                new TimeInterval(nowMicro - TimeUnit.DAYS.toMicros(1), nowMicro, dateString);
+        timeIntervals.add(today);
+
+        // Add yesterday as a baseline time interval for analysis
+        long oneDayAgo = nowMicro - TimeUnit.DAYS.toMicros(1);
+        String dateStringYesterday = new SimpleDateFormat(
+                "MM-dd-yyyy").format(new Date(TimeUnit.MICROSECONDS.toMillis(oneDayAgo)));
+        TimeInterval yesterday = new TimeInterval(
+                oneDayAgo - TimeUnit.DAYS.toMicros(1), oneDayAgo, dateStringYesterday);
+        timeIntervals.add(yesterday);
+
+        // Add last week as a baseline time interval for analysis
+        long oneWeek = TimeUnit.DAYS.toMicros(7);
+        long oneWeekAgo = nowMicro - oneWeek;
+        TimeInterval lastWeek = new TimeInterval(oneWeekAgo - oneWeek, oneWeekAgo, LAST_WEEK);
+        timeIntervals.add(lastWeek);
+
+        for (Key testKey : allTestKeys) {
+            List<PerformanceSummary> perfSummaries = new ArrayList<>();
+            List<String> labels = new ArrayList<>();
+            labels.add("");
+            for (TimeInterval interval : timeIntervals) {
+                PerformanceSummary perfSummary = new PerformanceSummary();
+                PerformanceUtil.updatePerformanceSummary(
+                        testKey.getName(), interval.start, interval.end, null, perfSummary);
+                if (perfSummary.size() == 0) {
+                    continue;
+                }
+                perfSummaries.add(perfSummary);
+                labels.add(interval.label);
+            }
+            String body = getPeformanceSummary(testKey.getName(), perfSummaries, labels);
+            if (body == null || body.equals("")) {
+                continue;
+            }
+            List<String> emails = EmailHelper.getSubscriberEmails(testKey);
+            if (emails.size() == 0) {
+                continue;
+            }
+            String subject = SUBJECT_PREFIX + testKey.getName();
+            EmailHelper.send(emails, subject, body);
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/util/DatastoreHelper.java b/src/main/java/com/android/vts/util/DatastoreHelper.java
new file mode 100644
index 0000000..4710b88
--- /dev/null
+++ b/src/main/java/com/android/vts/util/DatastoreHelper.java
@@ -0,0 +1,457 @@
+/**
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.vts.util;
+
+import com.android.vts.entity.CoverageEntity;
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestPlanEntity;
+import com.android.vts.entity.TestPlanRunEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.entity.TestRunEntity.TestRunType;
+import com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage;
+import com.android.vts.proto.VtsReportMessage.CoverageReportMessage;
+import com.android.vts.proto.VtsReportMessage.LogMessage;
+import com.android.vts.proto.VtsReportMessage.ProfilingReportMessage;
+import com.android.vts.proto.VtsReportMessage.TestCaseReportMessage;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.android.vts.proto.VtsReportMessage.TestPlanReportMessage;
+import com.android.vts.proto.VtsReportMessage.TestReportMessage;
+import com.google.appengine.api.datastore.DatastoreFailureException;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.DatastoreTimeoutException;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.KeyRange;
+import com.google.appengine.api.datastore.PropertyProjection;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.api.datastore.Transaction;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** DatastoreHelper, a helper class for interacting with Cloud Datastore. */
+public class DatastoreHelper {
+    protected static final Logger logger = Logger.getLogger(DatastoreHelper.class.getName());
+    public static final int MAX_WRITE_RETRIES = 5;
+
+    /**
+     * Returns true if there are data points newer than lowerBound in the results table.
+     *
+     * @param parentKey The parent key to use in the query.
+     * @param kind The query entity kind.
+     * @param lowerBound The (exclusive) lower time bound, long, microseconds.
+     * @return boolean True if there are newer data points.
+     * @throws IOException
+     */
+    public static boolean hasNewer(Key parentKey, String kind, Long lowerBound) throws IOException {
+        if (lowerBound == null || lowerBound <= 0)
+            return false;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Key startKey = KeyFactory.createKey(parentKey, kind, lowerBound);
+        Filter startFilter = new FilterPredicate(
+                Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, startKey);
+        Query q = new Query(kind).setAncestor(parentKey).setFilter(startFilter).setKeysOnly();
+        return datastore.prepare(q).countEntities(FetchOptions.Builder.withLimit(1)) > 0;
+    }
+
+    /**
+     * Returns true if there are data points older than upperBound in the table.
+     *
+     * @param parentKey The parent key to use in the query.
+     * @param kind The query entity kind.
+     * @param upperBound The (exclusive) upper time bound, long, microseconds.
+     * @return boolean True if there are older data points.
+     * @throws IOException
+     */
+    public static boolean hasOlder(Key parentKey, String kind, Long upperBound) throws IOException {
+        if (upperBound == null || upperBound <= 0)
+            return false;
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Key endKey = KeyFactory.createKey(parentKey, kind, upperBound);
+        Filter endFilter =
+                new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.LESS_THAN, endKey);
+        Query q = new Query(kind).setAncestor(parentKey).setFilter(endFilter).setKeysOnly();
+        return datastore.prepare(q).countEntities(FetchOptions.Builder.withLimit(1)) > 0;
+    }
+
+    /**
+     * Determines if any entities match the provided query.
+     *
+     * @param query The query to test.
+     * @return true if entities match the query.
+     */
+    public static boolean hasEntities(Query query) {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        FetchOptions limitOne = FetchOptions.Builder.withLimit(1);
+        return datastore.prepare(query).countEntities(limitOne) > 0;
+    }
+
+    /**
+     * Get all of the target product names.
+     *
+     * @return a list of all device product names.
+     */
+    public static List<String> getAllProducts() {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Query query = new Query(DeviceInfoEntity.KIND)
+                              .addProjection(new PropertyProjection(
+                                      DeviceInfoEntity.PRODUCT, String.class))
+                              .setDistinct(true);
+        List<String> devices = new ArrayList<>();
+        for (Entity e : datastore.prepare(query).asIterable()) {
+            devices.add((String) e.getProperty(DeviceInfoEntity.PRODUCT));
+        }
+        return devices;
+    }
+
+    /**
+     * Get all of the devices branches.
+     *
+     * @return a list of all branches.
+     */
+    public static List<String> getAllBranches() {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Query query = new Query(DeviceInfoEntity.KIND)
+                              .addProjection(
+                                      new PropertyProjection(DeviceInfoEntity.BRANCH, String.class))
+                              .setDistinct(true);
+        List<String> devices = new ArrayList<>();
+        for (Entity e : datastore.prepare(query).asIterable()) {
+            devices.add((String) e.getProperty(DeviceInfoEntity.BRANCH));
+        }
+        return devices;
+    }
+
+    /**
+     * Get all of the device build flavors.
+     *
+     * @return a list of all device build flavors.
+     */
+    public static List<String> getAllBuildFlavors() {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Query query = new Query(DeviceInfoEntity.KIND)
+                              .addProjection(new PropertyProjection(
+                                      DeviceInfoEntity.BUILD_FLAVOR, String.class))
+                              .setDistinct(true);
+        List<String> devices = new ArrayList<>();
+        for (Entity e : datastore.prepare(query).asIterable()) {
+            devices.add((String) e.getProperty(DeviceInfoEntity.BUILD_FLAVOR));
+        }
+        return devices;
+    }
+
+    /**
+     * Upload data from a test report message
+     *
+     * @param report The test report containing data to upload.
+     */
+    public static void insertTestReport(TestReportMessage report) {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Set<Entity> puts = new HashSet<>();
+
+        if (!report.hasStartTimestamp() || !report.hasEndTimestamp() || !report.hasTest()
+                || !report.hasHostInfo() || !report.hasBuildInfo()) {
+            // missing information
+            return;
+        }
+        long startTimestamp = report.getStartTimestamp();
+        long endTimestamp = report.getEndTimestamp();
+        String testName = report.getTest().toStringUtf8();
+        String testBuildId = report.getBuildInfo().getId().toStringUtf8();
+        String hostName = report.getHostInfo().getHostname().toStringUtf8();
+
+        Entity testEntity = new TestEntity(testName).toEntity();
+        List<Long> testCaseIds = new ArrayList<>();
+
+        Key testRunKey = KeyFactory.createKey(
+                testEntity.getKey(), TestRunEntity.KIND, report.getStartTimestamp());
+
+        long passCount = 0;
+        long failCount = 0;
+        long coveredLineCount = 0;
+        long totalLineCount = 0;
+
+        List<TestCaseRunEntity> testCases = new ArrayList<>();
+
+        // Process test cases
+        for (TestCaseReportMessage testCase : report.getTestCaseList()) {
+            String testCaseName = testCase.getName().toStringUtf8();
+            TestCaseResult result = testCase.getTestResult();
+            // Track global pass/fail counts
+            if (result == TestCaseResult.TEST_CASE_RESULT_PASS) {
+                ++passCount;
+            } else if (result != TestCaseResult.TEST_CASE_RESULT_SKIP) {
+                ++failCount;
+            }
+            String systraceLink = null;
+            if (testCase.getSystraceCount() > 0
+                    && testCase.getSystraceList().get(0).getUrlCount() > 0) {
+                systraceLink = testCase.getSystraceList().get(0).getUrl(0).toStringUtf8();
+            }
+            // Process coverage data for test case
+            for (CoverageReportMessage coverage : testCase.getCoverageList()) {
+                CoverageEntity coverageEntity =
+                        CoverageEntity.fromCoverageReport(testRunKey, testCaseName, coverage);
+                if (coverageEntity == null) {
+                    logger.log(Level.WARNING, "Invalid coverage report in test run " + testRunKey);
+                    continue;
+                }
+                coveredLineCount += coverageEntity.coveredLineCount;
+                totalLineCount += coverageEntity.totalLineCount;
+                puts.add(coverageEntity.toEntity());
+            }
+            // Process profiling data for test case
+            for (ProfilingReportMessage profiling : testCase.getProfilingList()) {
+                ProfilingPointRunEntity profilingEntity =
+                        ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
+                if (profilingEntity == null) {
+                    logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey);
+                }
+                puts.add(profilingEntity.toEntity());
+            }
+
+            int lastIndex = testCases.size() - 1;
+            if (lastIndex < 0 || testCases.get(lastIndex).isFull()) {
+                KeyRange keys = datastore.allocateIds(TestCaseRunEntity.KIND, 1);
+                testCaseIds.add(keys.getStart().getId());
+                testCases.add(new TestCaseRunEntity(keys.getStart()));
+                ++lastIndex;
+            }
+            TestCaseRunEntity testCaseEntity = testCases.get(lastIndex);
+            testCaseEntity.addTestCase(testCaseName, result.getNumber());
+            testCaseEntity.setSystraceUrl(systraceLink);
+        }
+        List<Entity> testCasePuts = new ArrayList<>();
+        for (TestCaseRunEntity testCaseEntity : testCases) {
+            testCasePuts.add(testCaseEntity.toEntity());
+        }
+        datastore.put(testCasePuts);
+
+        // Process device information
+        TestRunType testRunType = null;
+        for (AndroidDeviceInfoMessage device : report.getDeviceInfoList()) {
+            DeviceInfoEntity deviceInfoEntity =
+                    DeviceInfoEntity.fromDeviceInfoMessage(testRunKey, device);
+            if (deviceInfoEntity == null) {
+                logger.log(Level.WARNING, "Invalid device info in test run " + testRunKey);
+            }
+
+            // Run type on devices must be the same, else set to OTHER
+            TestRunType runType = TestRunType.fromBuildId(deviceInfoEntity.buildId);
+            if (testRunType == null) {
+                testRunType = runType;
+            } else if (runType != testRunType) {
+                testRunType = TestRunType.OTHER;
+            }
+            puts.add(deviceInfoEntity.toEntity());
+        }
+
+        // Overall run type should be determined by the device builds unless test build is OTHER
+        if (testRunType == null) {
+            testRunType = TestRunType.fromBuildId(testBuildId);
+        } else if (TestRunType.fromBuildId(testBuildId) == TestRunType.OTHER) {
+            testRunType = TestRunType.OTHER;
+        }
+
+        // Process global coverage data
+        for (CoverageReportMessage coverage : report.getCoverageList()) {
+            CoverageEntity coverageEntity =
+                    CoverageEntity.fromCoverageReport(testRunKey, new String(), coverage);
+            if (coverageEntity == null) {
+                logger.log(Level.WARNING, "Invalid coverage report in test run " + testRunKey);
+                continue;
+            }
+            coveredLineCount += coverageEntity.coveredLineCount;
+            totalLineCount += coverageEntity.totalLineCount;
+            puts.add(coverageEntity.toEntity());
+        }
+
+        // Process global profiling data
+        for (ProfilingReportMessage profiling : report.getProfilingList()) {
+            ProfilingPointRunEntity profilingEntity =
+                    ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
+            if (profilingEntity == null) {
+                logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey);
+            }
+            puts.add(profilingEntity.toEntity());
+        }
+
+        List<String> logLinks = new ArrayList<>();
+        // Process log data
+        for (LogMessage log : report.getLogList()) {
+            if (!log.hasUrl())
+                continue;
+            logLinks.add(log.getUrl().toStringUtf8());
+        }
+
+        TestRunEntity testRunEntity = new TestRunEntity(testEntity.getKey(), testRunType,
+                startTimestamp, endTimestamp, testBuildId, hostName, passCount, failCount,
+                testCaseIds, logLinks, coveredLineCount, totalLineCount);
+        puts.add(testRunEntity.toEntity());
+
+        int retries = 0;
+        while (true) {
+            Transaction txn = datastore.beginTransaction();
+            try {
+                // Check if test already exists in the database
+                try {
+                    datastore.get(testEntity.getKey());
+                } catch (EntityNotFoundException e) {
+                    puts.add(testEntity);
+                }
+                datastore.put(puts);
+                txn.commit();
+                break;
+            } catch (ConcurrentModificationException | DatastoreFailureException
+                    | DatastoreTimeoutException e) {
+                puts.remove(testEntity);
+                logger.log(Level.WARNING, "Retrying test run insert: " + testEntity.getKey());
+                if (retries++ >= MAX_WRITE_RETRIES) {
+                    logger.log(Level.SEVERE,
+                            "Exceeded maximum test run retries: " + testEntity.getKey());
+                    throw e;
+                }
+            } finally {
+                if (txn.isActive()) {
+                    logger.log(Level.WARNING,
+                            "Transaction rollback forced for run: " + testRunEntity.key);
+                    txn.rollback();
+                }
+            }
+        }
+    }
+
+    /**
+     * Upload data from a test plan report message
+     *
+     * @param report The test plan report containing data to upload.
+     */
+    public static void insertTestPlanReport(TestPlanReportMessage report) {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        List<Entity> puts = new ArrayList<>();
+
+        List<String> testModules = report.getTestModuleNameList();
+        List<Long> testTimes = report.getTestModuleStartTimestampList();
+        if (testModules.size() != testTimes.size() || !report.hasTestPlanName()) {
+            logger.log(Level.WARNING, "TestPlanReportMessage is missing information.");
+            return;
+        }
+
+        String testPlanName = report.getTestPlanName();
+        Entity testPlanEntity = new TestPlanEntity(testPlanName).toEntity();
+        List<Key> testRunKeys = new ArrayList<>();
+        for (int i = 0; i < testModules.size(); i++) {
+            String test = testModules.get(i);
+            long time = testTimes.get(i);
+            Key parentKey = KeyFactory.createKey(TestEntity.KIND, test);
+            Key testRunKey = KeyFactory.createKey(parentKey, TestRunEntity.KIND, time);
+            testRunKeys.add(testRunKey);
+        }
+        Map<Key, Entity> testRuns = datastore.get(testRunKeys);
+        long passCount = 0;
+        long failCount = 0;
+        long startTimestamp = -1;
+        long endTimestamp = -1;
+        String testBuildId = null;
+        TestRunType type = null;
+        Set<DeviceInfoEntity> devices = new HashSet<>();
+        for (Key testRunKey : testRuns.keySet()) {
+            TestRunEntity testRun = TestRunEntity.fromEntity(testRuns.get(testRunKey));
+            if (testRun == null) {
+                continue; // not a valid test run
+            }
+            passCount += testRun.passCount;
+            failCount += testRun.failCount;
+            if (startTimestamp < 0 || testRunKey.getId() < startTimestamp) {
+                startTimestamp = testRunKey.getId();
+            }
+            if (endTimestamp < 0 || testRun.endTimestamp > endTimestamp) {
+                endTimestamp = testRun.endTimestamp;
+            }
+            if (type == null) {
+                type = testRun.type;
+            } else if (type != testRun.type) {
+                type = TestRunType.OTHER;
+            }
+            testBuildId = testRun.testBuildId;
+            Query deviceInfoQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRunKey);
+            for (Entity deviceInfoEntity : datastore.prepare(deviceInfoQuery).asIterable()) {
+                DeviceInfoEntity device = DeviceInfoEntity.fromEntity(deviceInfoEntity);
+                if (device == null) {
+                    continue; // invalid entity
+                }
+                devices.add(device);
+            }
+        }
+        if (startTimestamp < 0 || testBuildId == null || type == null) {
+            logger.log(Level.WARNING, "Couldn't infer test run information from runs.");
+            return;
+        }
+        TestPlanRunEntity testPlanRun = new TestPlanRunEntity(testPlanEntity.getKey(), testPlanName,
+                type, startTimestamp, endTimestamp, testBuildId, passCount, failCount, testRunKeys);
+
+        // Create the device infos.
+        for (DeviceInfoEntity device : devices) {
+            puts.add(device.copyWithParent(testPlanRun.key).toEntity());
+        }
+        puts.add(testPlanRun.toEntity());
+
+        int retries = 0;
+        while (true) {
+            Transaction txn = datastore.beginTransaction();
+            try {
+                // Check if test already exists in the database
+                try {
+                    datastore.get(testPlanEntity.getKey());
+                } catch (EntityNotFoundException e) {
+                    puts.add(testPlanEntity);
+                }
+                datastore.put(puts);
+                txn.commit();
+                break;
+            } catch (ConcurrentModificationException | DatastoreFailureException
+                    | DatastoreTimeoutException e) {
+                puts.remove(testPlanEntity);
+                logger.log(Level.WARNING, "Retrying test plan insert: " + testPlanEntity.getKey());
+                if (retries++ >= MAX_WRITE_RETRIES) {
+                    logger.log(Level.SEVERE,
+                            "Exceeded maximum test plan retries: " + testPlanEntity.getKey());
+                    throw e;
+                }
+            } finally {
+                if (txn.isActive()) {
+                    logger.log(Level.WARNING,
+                            "Transaction rollback forced for plan run: " + testPlanRun.key);
+                    txn.rollback();
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/util/EmailHelper.java b/src/main/java/com/android/vts/util/EmailHelper.java
new file mode 100644
index 0000000..dd14d19
--- /dev/null
+++ b/src/main/java/com/android/vts/util/EmailHelper.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.vts.util;
+
+import com.android.vts.entity.UserFavoriteEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import org.apache.commons.lang.StringUtils;
+
+/** EmailHelper, a helper class for building and sending emails. */
+public class EmailHelper {
+    protected static final Logger logger = Logger.getLogger(EmailHelper.class.getName());
+    protected static final String DEFAULT_EMAIL = System.getProperty("DEFAULT_EMAIL");
+    protected static final String EMAIL_DOMAIN = System.getProperty("EMAIL_DOMAIN");
+    protected static final String SENDER_EMAIL = System.getProperty("SENDER_EMAIL");
+    private static final String VTS_EMAIL_NAME = "VTS Alert Bot";
+
+    /**
+     * Fetches the list of subscriber email addresses for a test.
+     *
+     * @param testKey The key for the test for which to fetch the email addresses.
+     * @returns List of email addresses (String).
+     * @throws IOException
+     */
+    public static List<String> getSubscriberEmails(Key testKey) throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Filter testFilter =
+                new FilterPredicate(UserFavoriteEntity.TEST_KEY, FilterOperator.EQUAL, testKey);
+        Query favoritesQuery = new Query(UserFavoriteEntity.KIND).setFilter(testFilter);
+        Set<String> emailSet = new HashSet<>();
+        if (!StringUtils.isBlank(DEFAULT_EMAIL)) {
+            emailSet.add(DEFAULT_EMAIL);
+        }
+        for (Entity favorite : datastore.prepare(favoritesQuery).asIterable()) {
+            UserFavoriteEntity favoriteEntity = UserFavoriteEntity.fromEntity(favorite);
+            if (favoriteEntity != null && favoriteEntity.user != null
+                    && favoriteEntity.user.getEmail().endsWith(EMAIL_DOMAIN)) {
+                emailSet.add(favoriteEntity.user.getEmail());
+            }
+        }
+        return new ArrayList<>(emailSet);
+    }
+
+    /**
+     * Sends an email to the specified email address to notify of a test status change.
+     *
+     * @param emails List of subscriber email addresses (byte[]) to which the email should be sent.
+     * @param subject The email subject field, string.
+     * @param body The html (string) body to send in the email.
+     * @returns The Message object to be sent.
+     * @throws MessagingException, UnsupportedEncodingException
+     */
+    public static Message composeEmail(List<String> emails, String subject, String body)
+            throws MessagingException, UnsupportedEncodingException {
+        if (emails.size() == 0) {
+            throw new MessagingException("No subscriber email addresses provided");
+        }
+        Properties props = new Properties();
+        Session session = Session.getDefaultInstance(props, null);
+
+        Message msg = new MimeMessage(session);
+        for (String email : emails) {
+            try {
+                msg.addRecipient(Message.RecipientType.TO, new InternetAddress(email, email));
+            } catch (MessagingException | UnsupportedEncodingException e) {
+                // Gracefully continue when a subscriber email is invalid.
+                logger.log(Level.WARNING, "Error sending email to recipient " + email + " : ", e);
+            }
+        }
+        msg.setFrom(new InternetAddress(SENDER_EMAIL, VTS_EMAIL_NAME));
+        msg.setSubject(subject);
+        msg.setContent(body, "text/html; charset=utf-8");
+        return msg;
+    }
+
+    /**
+     * Sends an email.
+     *
+     * @param msg Message object to send.
+     * @returns true if the message sends successfully, false otherwise
+     */
+    public static boolean send(Message msg) {
+        try {
+            Transport.send(msg);
+        } catch (MessagingException e) {
+            logger.log(Level.WARNING, "Error sending email : ", e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Sends a list of emails and logs any failures.
+     *
+     * @param messages List of Message objects to be sent.
+     */
+    public static void sendAll(List<Message> messages) {
+        for (Message msg : messages) {
+            send(msg);
+        }
+    }
+
+    /**
+     * Sends an email.
+     *
+     * @param recipients List of email address strings to which an email will be sent.
+     * @param subject The subject of the email.
+     * @param body The body of the email.
+     * @returns true if the message sends successfully, false otherwise
+     */
+    public static boolean send(List<String> recipients, String subject, String body) {
+        try {
+            Message msg = composeEmail(recipients, subject, body);
+            return send(msg);
+        } catch (MessagingException | UnsupportedEncodingException e) {
+            logger.log(Level.WARNING, "Error composing email : ", e);
+            return false;
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/util/FilterUtil.java b/src/main/java/com/android/vts/util/FilterUtil.java
new file mode 100644
index 0000000..5ca1bbb
--- /dev/null
+++ b/src/main/java/com/android/vts/util/FilterUtil.java
@@ -0,0 +1,321 @@
+/**
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.vts.util;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.gson.Gson;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.http.HttpServletRequest;
+
+/** FilterUtil, a helper class for parsing and matching search queries to data. */
+public class FilterUtil {
+    protected static final Logger logger = Logger.getLogger(FilterUtil.class.getName());
+    /** Key class to represent a filter token. */
+    public enum FilterKey {
+        DEVICE_BUILD_ID("deviceBuildId", DeviceInfoEntity.BUILD_ID, true),
+        BRANCH("branch", DeviceInfoEntity.BRANCH, true),
+        TARGET("device", DeviceInfoEntity.BUILD_FLAVOR, true),
+        VTS_BUILD_ID("testBuildId", TestRunEntity.TEST_BUILD_ID, false),
+        HOSTNAME("hostname", TestRunEntity.HOST_NAME, false),
+        PASSING("passing", TestRunEntity.PASS_COUNT, false),
+        NONPASSING("nonpassing", TestRunEntity.FAIL_COUNT, false);
+
+        private static final Map<String, FilterKey> keyMap;
+
+        static {
+            keyMap = new HashMap<>();
+            for (FilterKey k : EnumSet.allOf(FilterKey.class)) {
+                keyMap.put(k.keyString, k);
+            }
+        }
+
+        /**
+         * Test if a string is a valid device key.
+         *
+         * @param keyString The key string.
+         * @return True if they key string matches a key and the key is a device filter.
+         */
+        public static boolean isDeviceKey(String keyString) {
+            return keyMap.containsKey(keyString) && keyMap.get(keyString).isDevice;
+        }
+
+        /**
+         * Test if a string is a valid test key.
+         *
+         * @param keyString The key string.
+         * @return True if they key string matches a key and the key is a test filter.
+         */
+        public static boolean isTestKey(String keyString) {
+            return keyMap.containsKey(keyString) && !keyMap.get(keyString).isDevice;
+        }
+
+        /**
+         * Parses a key string into a key.
+         *
+         * @param keyString The key string.
+         * @return The key matching the key string.
+         */
+        public static FilterKey parse(String keyString) {
+            return keyMap.get(keyString);
+        }
+
+        private final String keyString;
+        private final String property;
+        private final boolean isDevice;
+
+        /**
+         * Constructs a key with the specified key string.
+         *
+         * @param keyString The identifying key string.
+         * @param propertyName The name of the property to match.
+         */
+        private FilterKey(String keyString, String propertyName, boolean isDevice) {
+            this.keyString = keyString;
+            this.property = propertyName;
+            this.isDevice = isDevice;
+        }
+
+        public Filter getFilterForString(String matchString) {
+            return new FilterPredicate(this.property, FilterOperator.EQUAL, matchString);
+        }
+
+        public Filter getFilterForNumber(long matchNumber) {
+            return new FilterPredicate(this.property, FilterOperator.EQUAL, matchNumber);
+        }
+    }
+
+    /**
+     * Get a filter on devices from a user search query.
+     *
+     * @param parameterMap The key-value map of url parameters.
+     * @return A filter with the values from the user search parameters.
+     */
+    public static Filter getUserDeviceFilter(Map<String, Object> parameterMap) {
+        Filter deviceFilter = null;
+        for (String key : parameterMap.keySet()) {
+            if (!FilterKey.isDeviceKey(key))
+                continue;
+            String[] values = (String[]) parameterMap.get(key);
+            if (values.length == 0)
+                continue;
+            String value = values[0];
+            FilterKey filterKey = FilterKey.parse(key);
+            Filter f = filterKey.getFilterForString(value);
+            if (deviceFilter == null) {
+                deviceFilter = f;
+            } else {
+                deviceFilter = CompositeFilterOperator.and(deviceFilter, f);
+            }
+        }
+        return deviceFilter;
+    }
+
+    /**
+     * Get a filter on test runs from a user search query.
+     *
+     * @param parameterMap The key-value map of url parameters.
+     * @return A filter with the values from the user search parameters.
+     */
+    public static Filter getUserTestFilter(Map<String, Object> parameterMap) {
+        Filter testFilter = null;
+        for (String key : parameterMap.keySet()) {
+            if (!FilterKey.isTestKey(key))
+                continue;
+            String[] values = (String[]) parameterMap.get(key);
+            if (values.length == 0)
+                continue;
+            String stringValue = values[0];
+            FilterKey filterKey = FilterKey.parse(key);
+            Filter f = null;
+            switch (filterKey) {
+                case NONPASSING:
+                case PASSING:
+                    try {
+                        Long value = Long.parseLong(stringValue);
+                        f = filterKey.getFilterForNumber(value);
+                    } catch (NumberFormatException e) {
+                        // invalid number
+                    }
+                    break;
+                case HOSTNAME:
+                case VTS_BUILD_ID:
+                    f = filterKey.getFilterForString(stringValue.toLowerCase());
+                    break;
+                default:
+                    break;
+            }
+            if (testFilter == null) {
+                testFilter = f;
+            } else if (f != null) {
+                testFilter = CompositeFilterOperator.and(testFilter, f);
+            }
+        }
+        return testFilter;
+    }
+
+    /**
+     * Get a filter on the test run type.
+     *
+     * @param showPresubmit True to display presubmit tests.
+     * @param showPostsubmit True to display postsubmit tests.
+     * @param unfiltered True if no filtering should be applied.
+     * @return A filter on the test type.
+     */
+    public static Filter getTestTypeFilter(
+            boolean showPresubmit, boolean showPostsubmit, boolean unfiltered) {
+        if (unfiltered) {
+            return null;
+        } else if (showPresubmit && !showPostsubmit) {
+            return new FilterPredicate(TestRunEntity.TYPE, FilterOperator.EQUAL,
+                    TestRunEntity.TestRunType.PRESUBMIT.getNumber());
+        } else if (showPostsubmit && !showPresubmit) {
+            return new FilterPredicate(TestRunEntity.TYPE, FilterOperator.EQUAL,
+                    TestRunEntity.TestRunType.POSTSUBMIT.getNumber());
+        } else {
+            List<Integer> types = new ArrayList<>();
+            types.add(TestRunEntity.TestRunType.PRESUBMIT.getNumber());
+            types.add(TestRunEntity.TestRunType.POSTSUBMIT.getNumber());
+            return new FilterPredicate(TestRunEntity.TYPE, FilterOperator.IN, types);
+        }
+    }
+
+    /**
+     * Get the time range filter to apply to a query.
+     *
+     * @param testKey The key of the parent TestEntity object.
+     * @param kind The kind to use for the filters.
+     * @param startTime The start time in microseconds, or null if unbounded.
+     * @param endTime The end time in microseconds, or null if unbounded.
+     * @param testRunFilter The existing filter on test runs to apply, or null.
+     * @return A filter to apply on test runs.
+     */
+    public static Filter getTimeFilter(
+            Key testKey, String kind, Long startTime, Long endTime, Filter testRunFilter) {
+        if (startTime == null && endTime == null) {
+            endTime = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+        }
+
+        Filter startFilter = null;
+        Filter endFilter = null;
+        Filter filter = null;
+        if (startTime != null) {
+            Key startKey = KeyFactory.createKey(testKey, kind, startTime);
+            startFilter = new FilterPredicate(
+                    Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN_OR_EQUAL, startKey);
+            filter = startFilter;
+        }
+        if (endTime != null) {
+            Key endKey = KeyFactory.createKey(testKey, kind, endTime);
+            endFilter = new FilterPredicate(
+                    Entity.KEY_RESERVED_PROPERTY, FilterOperator.LESS_THAN_OR_EQUAL, endKey);
+            filter = endFilter;
+        }
+        if (startFilter != null && endFilter != null) {
+            filter = CompositeFilterOperator.and(startFilter, endFilter);
+        }
+        if (testRunFilter != null) {
+            filter = CompositeFilterOperator.and(filter, testRunFilter);
+        }
+        return filter;
+    }
+
+    public static Filter getTimeFilter(Key testKey, String kind, Long startTime, Long endTime) {
+        return getTimeFilter(testKey, kind, startTime, endTime, null);
+    }
+
+    /**
+     * Get the list of keys matching the provided test filter and device filter.
+     *
+     * @param ancestorKey The ancestor key to use in the query.
+     * @param kind The entity kind to use in the test query.
+     * @param testFilter The filter to apply to the test runs.
+     * @param deviceFilter The filter to apply to associated devices.
+     * @param dir The sort direction of the returned list.
+     * @param maxSize The maximum number of entities to return.
+     * @return a list of keys matching the provided test and device filters.
+     */
+    public static List<Key> getMatchingKeys(Key ancestorKey, String kind, Filter testFilter,
+            Filter deviceFilter, Query.SortDirection dir, int maxSize) {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Set<Key> matchingTestKeys = new HashSet<>();
+        Query testQuery =
+                new Query(kind).setAncestor(ancestorKey).setFilter(testFilter).setKeysOnly();
+        for (Entity testRunKey : datastore.prepare(testQuery).asIterable()) {
+            matchingTestKeys.add(testRunKey.getKey());
+        }
+
+        Set<Key> allMatchingKeys;
+        if (deviceFilter == null) {
+            allMatchingKeys = matchingTestKeys;
+        } else {
+            allMatchingKeys = new HashSet<>();
+            Query deviceQuery = new Query(DeviceInfoEntity.KIND)
+                                        .setAncestor(ancestorKey)
+                                        .setFilter(deviceFilter)
+                                        .setKeysOnly();
+            for (Entity device : datastore.prepare(deviceQuery).asIterable()) {
+                if (matchingTestKeys.contains(device.getKey().getParent())) {
+                    allMatchingKeys.add(device.getKey().getParent());
+                }
+            }
+        }
+        List<Key> gets = new ArrayList<>(allMatchingKeys);
+        if (dir == Query.SortDirection.DESCENDING) {
+            Collections.sort(gets, Collections.reverseOrder());
+        } else {
+            Collections.sort(gets);
+        }
+        gets = gets.subList(0, Math.min(gets.size(), maxSize));
+        return gets;
+    }
+
+    /**
+     * Set the request with the provided key/value attribute map.
+     * @param request The request whose attributes to set.
+     * @param parameterMap The map from key to (Object) String[] value whose entries to parse.
+     */
+    public static void setAttributes(HttpServletRequest request, Map<String, Object> parameterMap) {
+        for (String key : parameterMap.keySet()) {
+            if (!FilterKey.isDeviceKey(key) && !FilterKey.isTestKey(key))
+                continue;
+            FilterKey filterKey = FilterKey.parse(key);
+            String[] values = (String[]) parameterMap.get(key);
+            if (values.length == 0)
+                continue;
+            String stringValue = values[0];
+            request.setAttribute(filterKey.keyString, new Gson().toJson(stringValue));
+        }
+    }
+}
diff --git a/src/main/java/com/android/vts/util/Graph.java b/src/main/java/com/android/vts/util/Graph.java
new file mode 100644
index 0000000..2fe98bb
--- /dev/null
+++ b/src/main/java/com/android/vts/util/Graph.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+/** Helper object for describing graph data. */
+public abstract class Graph {
+    public static final String VALUE_KEY = "values";
+    public static final String X_LABEL_KEY = "x_label";
+    public static final String Y_LABEL_KEY = "y_label";
+    public static final String IDS_KEY = "ids";
+    public static final String NAME_KEY = "name";
+    public static final String TYPE_KEY = "type";
+
+    public static enum GraphType { LINE_GRAPH, HISTOGRAM }
+
+    /**
+     * Get the graph type.
+     *
+     * @return The graph type.
+     */
+    public abstract GraphType getType();
+
+    /**
+     * Get the x axis label.
+     *
+     * @return The x axis label.
+     */
+    public abstract String getXLabel();
+
+    /**
+     * Get the y axis label.
+     *
+     * @return The y axis label.
+     */
+    public abstract String getYLabel();
+
+    /**
+     * Get the name of the graph.
+     *
+     * @return The name of the graph.
+     */
+    public abstract String getName();
+
+    /**
+     * Get the number of data points stored in the graph.
+     *
+     * @return The number of data points stored in the graph.
+     */
+    public abstract int size();
+
+    /**
+     * Add data to the graph.
+     *
+     * @param id The name of the graph.
+     * @param profilingPoint The ProfilingPointEntity containing data to add.
+     */
+    public abstract void addData(String id, ProfilingPointRunEntity profilingPoint);
+
+    /**
+     * Serializes the graph to json format.
+     *
+     * @return A JsonElement object representing the graph object.
+     */
+    public JsonObject toJson() {
+        JsonObject json = new JsonObject();
+        json.add(X_LABEL_KEY, new JsonPrimitive(getXLabel()));
+        json.add(Y_LABEL_KEY, new JsonPrimitive(getYLabel()));
+        json.add(NAME_KEY, new JsonPrimitive(getName()));
+        json.add(TYPE_KEY, new JsonPrimitive(getType().toString()));
+        return json;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/GraphSerializer.java b/src/main/java/com/android/vts/util/GraphSerializer.java
new file mode 100644
index 0000000..722b511
--- /dev/null
+++ b/src/main/java/com/android/vts/util/GraphSerializer.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+
+/** Serializer class for Graph objects. */
+public class GraphSerializer implements JsonSerializer<Graph> {
+    @Override
+    public JsonElement serialize(Graph src, Type typeOfSrc, JsonSerializationContext context) {
+        return src.toJson();
+    }
+}
diff --git a/src/main/java/com/android/vts/util/Histogram.java b/src/main/java/com/android/vts/util/Histogram.java
new file mode 100644
index 0000000..450714b
--- /dev/null
+++ b/src/main/java/com/android/vts/util/Histogram.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.google.common.primitives.Doubles;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.math3.stat.descriptive.rank.Percentile;
+
+/** Helper object for describing graph data. */
+public class Histogram extends Graph {
+    public static final String PERCENTILES_KEY = "percentiles";
+    public static final String PERCENTILE_VALUES_KEY = "percentile_values";
+    public static final String MIN_KEY = "min";
+    public static final String MAX_KEY = "max";
+
+    private List<Double> values;
+    private List<String> ids;
+    private String xLabel;
+    private String yLabel;
+    private String name;
+    private GraphType type = GraphType.HISTOGRAM;
+    private Double min = null;
+    private Double max = null;
+
+    public Histogram(String name) {
+        this.name = name;
+        this.values = new ArrayList<>();
+        this.ids = new ArrayList<>();
+    }
+
+    /**
+     * Get the x axis label.
+     *
+     * @return The x axis label.
+     */
+    @Override
+    public String getXLabel() {
+        return xLabel;
+    }
+
+    /**
+     * Get the graph type.
+     *
+     * @return The graph type.
+     */
+    @Override
+    public GraphType getType() {
+        return type;
+    }
+
+    /**
+     * Get the name of the graph.
+     *
+     * @return The name of the graph.
+     */
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Get the y axis label.
+     *
+     * @return The y axis label.
+     */
+    @Override
+    public String getYLabel() {
+        return yLabel;
+    }
+
+    /**
+     * Get the number of data points stored in the graph.
+     *
+     * @return The number of data points stored in the graph.
+     */
+    @Override
+    public int size() {
+        return values.size();
+    }
+
+    /**
+     * Get the minimum value.
+     *
+     * @return The minimum value.
+     */
+    public Double getMin() {
+        return min;
+    }
+
+    /**
+     * Get the maximum value.
+     *
+     * @return The maximum value.
+     */
+    public Double getMax() {
+        return max;
+    }
+
+    /**
+     * Add data to the graph.
+     *
+     * @param id The name of the graph.
+     * @param profilingPoint The ProfilingPointRunEntity containing data to add.
+     */
+    @Override
+    public void addData(String id, ProfilingPointRunEntity profilingPoint) {
+        if (profilingPoint.values.size() == 0)
+            return;
+        xLabel = profilingPoint.xLabel;
+        yLabel = profilingPoint.yLabel;
+        for (long v : profilingPoint.values) {
+            double value = v;
+            values.add(value);
+            ids.add(id);
+            if (max == null || value > max)
+                max = value;
+            if (min == null || value < min)
+                min = value;
+        }
+    }
+
+    /**
+     * Serializes the graph to json format.
+     *
+     * @return A JsonElement object representing the graph object.
+     */
+    @Override
+    public JsonObject toJson() {
+        int[] percentiles = {1, 2, 5, 10, 25, 50, 75, 90, 95, 98, 99};
+        double[] percentileValues = new double[percentiles.length];
+        double[] valueList = Doubles.toArray(values);
+        for (int i = 0; i < percentiles.length; i++) {
+            percentileValues[i] =
+                    Math.round(new Percentile().evaluate(valueList, percentiles[i]) * 1000d)
+                    / 1000d;
+        }
+        JsonObject json = super.toJson();
+        json.add(VALUE_KEY, new Gson().toJsonTree(values).getAsJsonArray());
+        json.add(PERCENTILES_KEY, new Gson().toJsonTree(percentiles).getAsJsonArray());
+        json.add(PERCENTILE_VALUES_KEY, new Gson().toJsonTree(percentileValues).getAsJsonArray());
+        json.add(IDS_KEY, new Gson().toJsonTree(ids).getAsJsonArray());
+        json.add(MIN_KEY, new JsonPrimitive(min));
+        json.add(MAX_KEY, new JsonPrimitive(max));
+        return json;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/LineGraph.java b/src/main/java/com/android/vts/util/LineGraph.java
new file mode 100644
index 0000000..22c64da
--- /dev/null
+++ b/src/main/java/com/android/vts/util/LineGraph.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Helper object for describing graph data. */
+public class LineGraph extends Graph {
+    public static final String TICKS_KEY = "ticks";
+
+    private List<ProfilingPointRunEntity> profilingRuns;
+    private List<String> ids;
+    private String xLabel;
+    private String yLabel;
+    private String name;
+    private GraphType type = GraphType.LINE_GRAPH;
+
+    public LineGraph(String name) {
+        this.name = name;
+        profilingRuns = new ArrayList<>();
+        ids = new ArrayList<>();
+    }
+
+    /**
+     * Get the x axis label.
+     *
+     * @return The x axis label.
+     */
+    @Override
+    public String getXLabel() {
+        return xLabel;
+    }
+
+    /**
+     * Get the graph type.
+     *
+     * @return The graph type.
+     */
+    @Override
+    public GraphType getType() {
+        return type;
+    }
+
+    /**
+     * Get the name of the graph.
+     *
+     * @return The name of the graph.
+     */
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Get the y axis label.
+     *
+     * @return The y axis label.
+     */
+    @Override
+    public String getYLabel() {
+        return yLabel;
+    }
+
+    /**
+     * Get the number of data points stored in the graph.
+     *
+     * @return The number of data points stored in the graph.
+     */
+    @Override
+    public int size() {
+        return profilingRuns.size();
+    }
+
+    /**
+     * Add data to the graph.
+     *
+     * @param id The name of the graph.
+     * @param profilingPoint The ProfilingPointRunEntity containing data to add.
+     */
+    @Override
+    public void addData(String id, ProfilingPointRunEntity profilingPoint) {
+        if (profilingPoint.values.size() == 0
+                || profilingPoint.values.size() != profilingPoint.labels.size())
+            return;
+        ids.add(id);
+        profilingRuns.add(profilingPoint);
+        xLabel = profilingPoint.xLabel;
+        yLabel = profilingPoint.yLabel;
+    }
+
+    /**
+     * Serializes the graph to json format.
+     *
+     * @return A JsonElement object representing the graph object.
+     */
+    @Override
+    public JsonObject toJson() {
+        JsonObject json = super.toJson();
+        // Use the most recent profiling vector to generate the labels
+        ProfilingPointRunEntity profilingRun = profilingRuns.get(profilingRuns.size() - 1);
+
+        List<String> axisTicks = new ArrayList<>();
+        Map<String, Integer> tickIndexMap = new HashMap<>();
+        for (int i = 0; i < profilingRun.labels.size(); i++) {
+            String label = profilingRun.labels.get(i);
+            axisTicks.add(label);
+            tickIndexMap.put(label, i);
+        }
+
+        long[][] lineGraphValues = new long[axisTicks.size()][profilingRuns.size()];
+        for (int reportIndex = 0; reportIndex < profilingRuns.size(); reportIndex++) {
+            ProfilingPointRunEntity pt = profilingRuns.get(reportIndex);
+            for (int i = 0; i < pt.labels.size(); i++) {
+                String label = pt.labels.get(i);
+
+                // Skip value if its label is not present
+                if (!tickIndexMap.containsKey(label))
+                    continue;
+                int labelIndex = tickIndexMap.get(label);
+
+                lineGraphValues[labelIndex][reportIndex] = pt.values.get(i);
+            }
+        }
+        json.add(VALUE_KEY, new Gson().toJsonTree(lineGraphValues).getAsJsonArray());
+        json.add(IDS_KEY, new Gson().toJsonTree(ids).getAsJsonArray());
+        json.add(TICKS_KEY, new Gson().toJsonTree(axisTicks));
+        return json;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/PerformanceSummary.java b/src/main/java/com/android/vts/util/PerformanceSummary.java
new file mode 100644
index 0000000..05ed0b7
--- /dev/null
+++ b/src/main/java/com/android/vts/util/PerformanceSummary.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.vts.util;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.google.appengine.api.datastore.Entity;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** PerformanceSummary, an object summarizing performance across profiling points for a test run. */
+public class PerformanceSummary {
+    protected static Logger logger = Logger.getLogger(PerformanceSummary.class.getName());
+    private Map<String, ProfilingPointSummary> summaryMap;
+    private Set<String> optionSplitKeys;
+
+    /** Creates a performance summary object. */
+    public PerformanceSummary() {
+        this.summaryMap = new HashMap<>();
+        this.optionSplitKeys = new HashSet<>();
+    }
+
+    /**
+     * Creates a performance summary object with the specified device name filter. If the specified
+     * name is null, then use no filter.
+     *
+     * @param optionSplitKeys A set of option keys to split on (i.e. profiling data with different
+     *     values corresponding to the option key will be analyzed as different profiling points).
+     */
+    public PerformanceSummary(Set<String> optionSplitKeys) {
+        this();
+        this.optionSplitKeys = optionSplitKeys;
+    }
+
+    /**
+     * Add the profiling data from a ProfilingPointRunEntity to the performance summary.
+     *
+     * @param profilingRun The Entity object whose data to add.
+     */
+    public void addData(Entity profilingRun) {
+        ProfilingPointRunEntity pt = ProfilingPointRunEntity.fromEntity(profilingRun);
+        if (pt == null)
+            return;
+        if (pt.regressionMode == VtsProfilingRegressionMode.VTS_REGRESSION_MODE_DISABLED) {
+            return;
+        }
+
+        String name = pt.name;
+        String optionSuffix = PerformanceUtil.getOptionAlias(pt, optionSplitKeys);
+
+        if (pt.labels != null) {
+            if (pt.labels.size() != pt.values.size()) {
+                logger.log(Level.WARNING, "Labels and values are different sizes.");
+                return;
+            }
+            if (!optionSuffix.equals("")) {
+                name += " (" + optionSuffix + ")";
+            }
+            if (!summaryMap.containsKey(name)) {
+                summaryMap.put(name, new ProfilingPointSummary());
+            }
+            summaryMap.get(name).update(pt);
+        } else {
+            // Use the option suffix as the table name.
+            // Group all profiling points together into one table
+            if (!summaryMap.containsKey(optionSuffix)) {
+                summaryMap.put(optionSuffix, new ProfilingPointSummary());
+            }
+            summaryMap.get(optionSuffix).updateLabel(pt, pt.name);
+        }
+    }
+
+    /**
+     * Adds a ProfilingPointSummary object into the summary map only if the key doesn't exist.
+     *
+     * @param key The name of the profiling point.
+     * @param summary The ProfilingPointSummary object to add into the summary map.
+     * @return True if the data was inserted into the performance summary, false otherwise.
+     */
+    public boolean insertProfilingPointSummary(String key, ProfilingPointSummary summary) {
+        if (!summaryMap.containsKey(key)) {
+            summaryMap.put(key, summary);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Gets the number of profiling points.
+     *
+     * @return The number of profiling points in the performance summary.
+     */
+    public int size() {
+        return summaryMap.size();
+    }
+
+    /**
+     * Gets the names of the profiling points.
+     *
+     * @return A string array of profiling point names.
+     */
+    public String[] getProfilingPointNames() {
+        String[] profilingNames = summaryMap.keySet().toArray(new String[summaryMap.size()]);
+        Arrays.sort(profilingNames);
+        return profilingNames;
+    }
+
+    /**
+     * Determines if a profiling point is described by the performance summary.
+     *
+     * @param profilingPointName The name of the profiling point.
+     * @return True if the profiling point is contained in the performance summary, else false.
+     */
+    public boolean hasProfilingPoint(String profilingPointName) {
+        return summaryMap.containsKey(profilingPointName);
+    }
+
+    /**
+     * Gets the profiling point summary by name.
+     *
+     * @param profilingPointName The name of the profiling point to fetch.
+     * @return The ProfilingPointSummary object describing the specified profiling point.
+     */
+    public ProfilingPointSummary getProfilingPointSummary(String profilingPointName) {
+        return summaryMap.get(profilingPointName);
+    }
+}
diff --git a/src/main/java/com/android/vts/util/PerformanceUtil.java b/src/main/java/com/android/vts/util/PerformanceUtil.java
new file mode 100644
index 0000000..02c2afa
--- /dev/null
+++ b/src/main/java/com/android/vts/util/PerformanceUtil.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.vts.util;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import java.io.IOException;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+import org.apache.commons.lang.StringUtils;
+
+/** PerformanceUtil, a helper class for analyzing profiling and performance data. */
+public class PerformanceUtil {
+    protected static Logger logger = Logger.getLogger(PerformanceUtil.class.getName());
+
+    private static final DecimalFormat FORMATTER;
+    private static final String NAME_DELIMITER = ", ";
+    private static final String OPTION_DELIMITER = "=";
+
+    /**
+     * Initialize the decimal formatter.
+     */
+    static {
+        FORMATTER = new DecimalFormat("#.##");
+        FORMATTER.setRoundingMode(RoundingMode.HALF_UP);
+    }
+
+    public static class TimeInterval {
+        public final long start;
+        public final long end;
+        public final String label;
+
+        public TimeInterval(long start, long end, String label) {
+            this.start = start;
+            this.end = end;
+            this.label = label;
+        }
+
+        public TimeInterval(long start, long end) {
+            this(start, end, "<span class='date-label'>"
+                            + Long.toString(TimeUnit.MICROSECONDS.toMillis(end)) + "</span>");
+        }
+    }
+
+    /**
+     * Creates the HTML for a table cell representing the percent change between two numbers.
+     *
+     * <p>Computes the percent change (after - before)/before * 100 and inserts it into a table cell
+     * with the specified style. The color of the cell is white if 'after' is less than before.
+     * Otherwise, the cell is colored red with opacity according to the percent change (100%+ delta
+     * means 100% opacity). If the before value is 0 and the after value is positive, then the color
+     * of the cell is 100% red to indicate an increase of undefined magnitude.
+     *
+     * @param baseline The baseline value observed.
+     * @param test The value to compare against the baseline.
+     * @param classNames A string containing HTML classes to apply to the table cell.
+     * @param style A string containing additional CSS styles.
+     * @returns An HTML string for a colored table cell containing the percent change.
+     */
+    public static String getPercentChangeHTML(double baseline, double test, String classNames,
+            String style, VtsProfilingRegressionMode mode) {
+        String pctChangeString = "0 %";
+        double alpha = 0;
+        double delta = test - baseline;
+        if (baseline != 0) {
+            double pctChange = delta / baseline;
+            alpha = pctChange * 2;
+            pctChangeString = FORMATTER.format(pctChange * 100) + " %";
+        } else if (delta != 0) {
+            // If the percent change is undefined, the cell will be solid red or white
+            alpha = (int) Math.signum(delta); // get the sign of the delta (+1, 0, -1)
+            pctChangeString = "";
+        }
+        if (mode == VtsProfilingRegressionMode.VTS_REGRESSION_MODE_DECREASING) {
+            alpha = -alpha;
+        }
+        String color = "background-color: rgba(255, 0, 0, " + alpha + "); ";
+        String html = "<td class='" + classNames + "' style='" + color + style + "'>";
+        html += pctChangeString + "</td>";
+        return html;
+    }
+
+    /**
+     * Compares a test StatSummary to a baseline StatSummary using best-case performance.
+     *
+     * @param baseline The StatSummary object containing initial values to compare against
+     * @param test The StatSummary object containing test values to be compared against the baseline
+     * @param innerClasses Class names to apply to cells on the inside of the grid
+     * @param outerClasses Class names to apply to cells on the outside of the grid
+     * @param innerStyles CSS styles to apply to cells on the inside of the grid
+     * @param outerStyles CSS styles to apply to cells on the outside of the grid
+     * @return HTML string representing the performance of the test versus the baseline
+     */
+    public static String getBestCasePerformanceComparisonHTML(StatSummary baseline,
+            StatSummary test, String innerClasses, String outerClasses, String innerStyles,
+            String outerStyles) {
+        if (test == null || baseline == null) {
+            return "<td></td><td></td><td></td><td></td>";
+        }
+        String row = "";
+        // Intensity of red color is a function of the relative (percent) change
+        // in the new value compared to the previous day's. Intensity is a linear function
+        // of percentage change, reaching a ceiling at 100% change (e.g. a doubling).
+        row += getPercentChangeHTML(baseline.getBestCase(), test.getBestCase(), innerClasses,
+                innerStyles, test.getRegressionMode());
+        row += "<td class='" + innerClasses + "' style='" + innerStyles + "'>";
+        row += FORMATTER.format(baseline.getBestCase());
+        row += "<td class='" + innerClasses + "' style='" + innerStyles + "'>";
+        row += FORMATTER.format(baseline.getMean());
+        row += "<td class='" + outerClasses + "' style='" + outerStyles + "'>";
+        row += FORMATTER.format(baseline.getStd()) + "</td>";
+        return row;
+    }
+
+    /**
+     * Compares a test StatSummary to a baseline StatSummary using average-case performance.
+     *
+     * @param baseline The StatSummary object containing initial values to compare against
+     * @param test The StatSummary object containing test values to be compared against the baseline
+     * @param innerClasses Class names to apply to cells on the inside of the grid
+     * @param outerClasses Class names to apply to cells on the outside of the grid
+     * @param innerStyles CSS styles to apply to cells on the inside of the grid
+     * @param outerStyles CSS styles to apply to cells on the outside of the grid
+     * @return HTML string representing the performance of the test versus the baseline
+     */
+    public static String getAvgCasePerformanceComparisonHTML(StatSummary baseline, StatSummary test,
+            String innerClasses, String outerClasses, String innerStyles, String outerStyles) {
+        if (test == null || baseline == null) {
+            return "<td></td><td></td><td></td><td></td>";
+        }
+        String row = "";
+        // Intensity of red color is a function of the relative (percent) change
+        // in the new value compared to the previous day's. Intensity is a linear function
+        // of percentage change, reaching a ceiling at 100% change (e.g. a doubling).
+        row += getPercentChangeHTML(baseline.getMean(), test.getMean(), innerClasses, innerStyles,
+                test.getRegressionMode());
+        row += "<td class='" + innerClasses + "' style='" + innerStyles + "'>";
+        row += FORMATTER.format(baseline.getBestCase());
+        row += "<td class='" + innerClasses + "' style='" + innerStyles + "'>";
+        row += FORMATTER.format(baseline.getMean());
+        row += "<td class='" + outerClasses + "' style='" + outerStyles + "'>";
+        row += FORMATTER.format(baseline.getStd()) + "</td>";
+        return row;
+    }
+
+    /**
+     * Updates a PerformanceSummary object with data in the specified window.
+     *
+     * @param testName The name of the table whose profiling vectors to retrieve.
+     * @param startTime The (inclusive) start time in microseconds to scan from.
+     * @param endTime The (inclusive) end time in microseconds at which to stop scanning.
+     * @param selectedDevice The name of the device whose data to query for, or null for unfiltered.
+     * @param perfSummary The PerformanceSummary object to update with data.
+     * @throws IOException
+     */
+    public static void updatePerformanceSummary(String testName, long startTime, long endTime,
+            String selectedDevice, PerformanceSummary perfSummary) throws IOException {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Key testKey = KeyFactory.createKey(TestEntity.KIND, testName);
+        Filter testTypeFilter = FilterUtil.getTestTypeFilter(false, true, false);
+        Filter runFilter = FilterUtil.getTimeFilter(
+                testKey, TestRunEntity.KIND, startTime, endTime, testTypeFilter);
+
+        Filter deviceFilter = null;
+        if (selectedDevice != null) {
+            deviceFilter = new FilterPredicate(
+                    DeviceInfoEntity.PRODUCT, FilterOperator.EQUAL, selectedDevice);
+        }
+        Query testRunQuery = new Query(TestRunEntity.KIND)
+                                     .setAncestor(testKey)
+                                     .setFilter(runFilter)
+                                     .setKeysOnly();
+        for (Entity testRun : datastore.prepare(testRunQuery).asIterable()) {
+            if (deviceFilter != null) {
+                Query deviceQuery = new Query(DeviceInfoEntity.KIND)
+                                            .setAncestor(testRun.getKey())
+                                            .setFilter(deviceFilter)
+                                            .setKeysOnly();
+                if (!DatastoreHelper.hasEntities(deviceQuery))
+                    continue;
+            }
+            Query q = new Query(ProfilingPointRunEntity.KIND).setAncestor(testRun.getKey());
+
+            for (Entity profilingRun : datastore.prepare(q).asIterable()) {
+                perfSummary.addData(profilingRun);
+            }
+        }
+    }
+
+    /**
+     * Generates a string of the values in optionsList which have matches in the profiling entity.
+     *
+     * @param profilingRun The entity for a profiling point run.
+     * @param optionKeys A list of keys to match against the optionsList key value pairs.
+     * @return The values in optionsList whose key match a key in optionKeys.
+     */
+    public static String getOptionAlias(
+            ProfilingPointRunEntity profilingRun, Set<String> optionKeys) {
+        String name = "";
+        List<String> nameSuffixes = new ArrayList<>();
+        if (profilingRun.options != null) {
+            for (String optionString : profilingRun.options) {
+                String[] optionParts = optionString.split(OPTION_DELIMITER);
+                if (optionParts.length != 2) {
+                    continue;
+                }
+                if (optionKeys.contains(optionParts[0].trim().toLowerCase())) {
+                    nameSuffixes.add(optionParts[1].trim().toLowerCase());
+                }
+            }
+            if (nameSuffixes.size() > 0) {
+                StringUtils.join(nameSuffixes, NAME_DELIMITER);
+                name += StringUtils.join(nameSuffixes, NAME_DELIMITER);
+            }
+        }
+        return name;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/ProfilingPointSummary.java b/src/main/java/com/android/vts/util/ProfilingPointSummary.java
new file mode 100644
index 0000000..6e3e8c9
--- /dev/null
+++ b/src/main/java/com/android/vts/util/ProfilingPointSummary.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/** Represents statistical summaries of each profiling point. */
+public class ProfilingPointSummary implements Iterable<StatSummary> {
+    private List<StatSummary> statSummaries;
+    private Map<String, Integer> labelIndices;
+    private List<String> labels;
+    private VtsProfilingRegressionMode regressionMode;
+    public String xLabel;
+    public String yLabel;
+
+    /** Initializes the summary with empty arrays. */
+    public ProfilingPointSummary() {
+        statSummaries = new ArrayList<>();
+        labelIndices = new HashMap<>();
+        labels = new ArrayList<>();
+    }
+
+    /**
+     * Determines if a data label is present in the profiling point.
+     *
+     * @param label The name of the label.
+     * @return true if the label is present, false otherwise.
+     */
+    public boolean hasLabel(String label) {
+        return labelIndices.containsKey(label);
+    }
+
+    /**
+     * Gets the statistical summary for a specified data label.
+     *
+     * @param label The name of the label.
+     * @return The StatSummary object if it exists, or null otherwise.
+     */
+    public StatSummary getStatSummary(String label) {
+        if (!hasLabel(label))
+            return null;
+        return statSummaries.get(labelIndices.get(label));
+    }
+
+    /**
+     * Gets the last-seen regression mode.
+     *
+     * @return The VtsProfilingRegressionMode value.
+     */
+    public VtsProfilingRegressionMode getRegressionMode() {
+        return regressionMode;
+    }
+
+    /**
+     * Updates the profiling summary with the data from a new profiling report.
+     *
+     * @param profilingRun The profiling point run entity object containing profiling data.
+     */
+    public void update(ProfilingPointRunEntity profilingRun) {
+        for (int i = 0; i < profilingRun.labels.size(); i++) {
+            String label = profilingRun.labels.get(i);
+            if (!labelIndices.containsKey(label)) {
+                StatSummary summary = new StatSummary(label, profilingRun.regressionMode);
+                labelIndices.put(label, statSummaries.size());
+                statSummaries.add(summary);
+            }
+            StatSummary summary = getStatSummary(label);
+            summary.updateStats(profilingRun.values.get(i));
+        }
+        this.regressionMode = profilingRun.regressionMode;
+        this.labels = profilingRun.labels;
+        this.xLabel = profilingRun.xLabel;
+        this.yLabel = profilingRun.yLabel;
+    }
+
+    /**
+     * Updates the profiling summary at a label with the data from a new profiling report.
+     *
+     * <p>Updates the summary specified by the label with all values provided in the report. If
+     * labels
+     * are provided in the report, they will be ignored -- all values are updated only to the
+     * provided
+     * label.
+     *
+     * @param profilingEntity The ProfilingPointRunEntity object containing profiling data.
+     * @param label The ByteString label for which all values in the report will be updated.
+     */
+    public void updateLabel(ProfilingPointRunEntity profilingEntity, String label) {
+        if (!labelIndices.containsKey(label)) {
+            StatSummary summary = new StatSummary(label, profilingEntity.regressionMode);
+            labelIndices.put(label, labels.size());
+            labels.add(label);
+            statSummaries.add(summary);
+        }
+        StatSummary summary = getStatSummary(label);
+        for (long value : profilingEntity.values) {
+            summary.updateStats(value);
+        }
+        this.regressionMode = profilingEntity.regressionMode;
+        this.xLabel = profilingEntity.xLabel;
+        this.yLabel = profilingEntity.yLabel;
+    }
+
+    /**
+     * Gets an iterator that returns stat summaries in the ordered the labels were specified in the
+     * ProfilingReportMessage objects.
+     */
+    @Override
+    public Iterator<StatSummary> iterator() {
+        Iterator<StatSummary> it = new Iterator<StatSummary>() {
+            private int currentIndex = 0;
+
+            @Override
+            public boolean hasNext() {
+                return labels != null && currentIndex < labels.size();
+            }
+
+            @Override
+            public StatSummary next() {
+                String label = labels.get(currentIndex++);
+                return statSummaries.get(labelIndices.get(label));
+            }
+        };
+        return it;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/StatSummary.java b/src/main/java/com/android/vts/util/StatSummary.java
new file mode 100644
index 0000000..04db7c4
--- /dev/null
+++ b/src/main/java/com/android/vts/util/StatSummary.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+
+/** Helper object for storing statistics. */
+public class StatSummary {
+    private String label;
+    private double min;
+    private double max;
+    private double mean;
+    private double var;
+    private int n;
+    private VtsProfilingRegressionMode regression_mode;
+
+    /**
+     * Initializes the statistical summary.
+     *
+     * <p>Sets the label as provided. Initializes the mean, variance, and n (number of values seen)
+     * to
+     * 0.
+     *
+     * @param label The (String) label to assign to the summary.
+     * @param mode The VtsProfilingRegressionMode to use when analyzing performance.
+     */
+    public StatSummary(String label, VtsProfilingRegressionMode mode) {
+        this.label = label;
+        this.min = Double.MAX_VALUE;
+        this.max = Double.MIN_VALUE;
+        this.mean = 0;
+        this.var = 0;
+        this.n = 0;
+        this.regression_mode = mode;
+    }
+
+    /**
+     * Update the mean and variance using Welford's single-pass method.
+     *
+     * @param value The observed value in the stream.
+     */
+    public void updateStats(double value) {
+        n += 1;
+        double oldMean = mean;
+        mean = oldMean + (value - oldMean) / n;
+        var = var + (value - mean) * (value - oldMean);
+        if (value < min)
+            min = value;
+        if (value > max)
+            max = value;
+    }
+
+    /**
+     * Gets the best case of the stream.
+     *
+     * @return The min or max.
+     */
+    public double getBestCase() {
+        switch (regression_mode) {
+            case VTS_REGRESSION_MODE_DECREASING:
+                return getMax();
+            default:
+                return getMin();
+        }
+    }
+
+    /**
+     * Gets the calculated min of the stream.
+     *
+     * @return The min.
+     */
+    public double getMin() {
+        return min;
+    }
+
+    /**
+     * Gets the calculated max of the stream.
+     *
+     * @return The max.
+     */
+    public double getMax() {
+        return max;
+    }
+
+    /**
+     * Gets the calculated mean of the stream.
+     *
+     * @return The mean.
+     */
+    public double getMean() {
+        return mean;
+    }
+
+    /**
+     * Gets the calculated standard deviation of the stream.
+     *
+     * @return The standard deviation.
+     */
+    public double getStd() {
+        return Math.sqrt(var / (n - 1));
+    }
+
+    /**
+     * Gets the number of elements that have passed through the stream.
+     *
+     * @return Number of elements.
+     */
+    public int getCount() {
+        return n;
+    }
+
+    /**
+     * Gets the label for the summarized statistics.
+     *
+     * @return The (string) label.
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * Gets the regression mode.
+     *
+     * @return The VtsProfilingRegressionMode value.
+     */
+    public VtsProfilingRegressionMode getRegressionMode() {
+        return regression_mode;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/TestResults.java b/src/main/java/com/android/vts/util/TestResults.java
new file mode 100644
index 0000000..0591bb6
--- /dev/null
+++ b/src/main/java/com/android/vts/util/TestResults.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestCaseRunEntity.TestCase;
+import com.android.vts.entity.TestRunEntity;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.android.vts.util.UrlUtil.LinkDisplay;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Key;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.commons.lang.StringUtils;
+
+/** Helper object for describing test results data. */
+public class TestResults {
+    private final Logger logger = Logger.getLogger(getClass().getName());
+
+    private List<TestRunEntity> testRuns; // list of all test runs
+    private Map<Key, List<TestCaseRunEntity>>
+            testCaseRunMap; // map from test run key to the test run information
+    private Map<Key, List<DeviceInfoEntity>> deviceInfoMap; // map from test run key to device info
+    private Map<String, Integer> testCaseNameMap; // map from test case name to its order
+    private Set<String> profilingPointNameSet; // set of profiling point names
+
+    public String testName;
+    public String[] headerRow; // row to display above the test results table
+    public String[][] timeGrid; // grid of data storing timestamps to render as dates
+    public String[][] durationGrid; // grid of data storing timestamps to render as time intervals
+    public String[][] summaryGrid; // grid of data displaying a summary of the test run
+    public String[][] resultsGrid; // grid of data displaying test case results
+    public String[] profilingPointNames; // list of profiling point names in the test run
+    public Map<String, List<String[]>> logInfoMap; // map from test run index to url/display pairs
+    public int[] totResultCounts; // array of test result counts for the tip-of-tree runs
+    public String totBuildId = ""; // build ID of tip-of-tree run
+    public long startTime = Long.MAX_VALUE; // oldest timestamp displayed in the results table
+    public long endTime = Long.MIN_VALUE; // newest timestamp displayed in the results table
+
+    // Row labels for the test time-formatted information.
+    private static final String[] TIME_INFO_NAMES = {"Test Start", "Test End"};
+
+    // Row labels for the test duration information.
+    private static final String[] DURATION_INFO_NAMES = {"<b>Test Duration</b>"};
+
+    // Row labels for the test summary grid.
+    private static final String[] SUMMARY_NAMES = {"Total", "Passing #", "Non-Passing #",
+            "Passing %", "Covered Lines", "Coverage %", "Logs"};
+
+    // Row labels for the device summary information in the table header.
+    private static final String[] HEADER_NAMES = {"<b>Stats Type \\ Device Build ID</b>", "Branch",
+            "Build Target", "Device", "ABI Target", "VTS Build ID", "Hostname"};
+
+    /**
+     * Create a test results object.
+     *
+     * @param testName The name of the test.
+     */
+    public TestResults(String testName) {
+        this.testName = testName;
+        this.testRuns = new ArrayList<>();
+        this.deviceInfoMap = new HashMap<>();
+        this.testCaseRunMap = new HashMap<>();
+        this.testCaseNameMap = new HashMap<>();
+        this.logInfoMap = new HashMap<>();
+        this.profilingPointNameSet = new HashSet<>();
+    }
+
+    /**
+     * Add a test run to the test results.
+     *
+     * @param testRun The Entity containing the test run information.
+     * @param testCaseRuns The collection of test case executions within the test run.
+     * @param deviceInfos The collection of device information entities for the test run.
+     * @param profilingPoints The collection of profiling point names for the test run.
+     */
+    public void addTestRun(Entity testRun, Iterable<Entity> testCaseRuns,
+            Iterable<Entity> deviceInfos, Iterable<Entity> profilingPoints) {
+        // Process the devices in the test run
+        List<DeviceInfoEntity> devices = new ArrayList<>();
+        for (Entity e : deviceInfos) {
+            DeviceInfoEntity deviceInfoEntity = DeviceInfoEntity.fromEntity(e);
+            if (deviceInfoEntity == null)
+                continue;
+            devices.add(deviceInfoEntity);
+        }
+
+        // Filter out test runs lacking device information
+        if (devices.size() == 0) {
+            logger.log(Level.WARNING, "No device info found for run: " + testRun.getKey());
+            return;
+        }
+        deviceInfoMap.put(testRun.getKey(), devices);
+
+        TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRun);
+        if (testRunEntity == null)
+            return;
+        if (testRunEntity.startTimestamp < startTime) {
+            startTime = testRunEntity.startTimestamp;
+        }
+        if (testRunEntity.endTimestamp > endTime) {
+            endTime = testRunEntity.endTimestamp;
+        }
+        testRuns.add(testRunEntity);
+        testCaseRunMap.put(testRun.getKey(), new ArrayList<TestCaseRunEntity>());
+
+        // Process the test cases in the test run
+        for (Entity e : testCaseRuns) {
+            TestCaseRunEntity testCaseRunEntity = TestCaseRunEntity.fromEntity(e);
+            if (testCaseRunEntity == null)
+                continue;
+            testCaseRunMap.get(testRun.getKey()).add(testCaseRunEntity);
+            for (TestCase testCase : testCaseRunEntity.testCases) {
+                if (!testCaseNameMap.containsKey(testCase.name)) {
+                    testCaseNameMap.put(testCase.name, testCaseNameMap.size());
+                }
+            }
+        }
+
+        // Process the profiling point observations in the test run
+        for (Entity e : profilingPoints) {
+            if (e.getKey().getName() != null) {
+                profilingPointNameSet.add(e.getKey().getName());
+            }
+        }
+    }
+
+    /** Creates a test case breakdown of the most recent test run. */
+    private void generateToTBreakdown() {
+        totResultCounts = new int[TestCaseResult.values().length];
+        if (testRuns.size() == 0)
+            return;
+
+        TestRunEntity mostRecentRun = testRuns.get(0);
+        List<TestCaseRunEntity> testCaseResults = testCaseRunMap.get(mostRecentRun.key);
+        List<DeviceInfoEntity> deviceInfos = deviceInfoMap.get(mostRecentRun.key);
+        if (deviceInfos.size() > 0) {
+            DeviceInfoEntity totDevice = deviceInfos.get(0);
+            totBuildId = totDevice.buildId;
+        }
+        // Count array for each test result
+        for (TestCaseRunEntity testCaseRunEntity : testCaseResults) {
+            for (TestCase testCase : testCaseRunEntity.testCases) {
+                totResultCounts[testCase.result]++;
+            }
+        }
+    }
+
+    /**
+     * Get the number of test runs observed.
+     *
+     * @return The number of test runs observed.
+     */
+    public int getSize() {
+        return testRuns.size();
+    }
+
+    /** Post-process the test runs to generate reports of the results. */
+    public void processReport() {
+        Comparator<TestRunEntity> comparator = new Comparator<TestRunEntity>() {
+            @Override
+            public int compare(TestRunEntity t1, TestRunEntity t2) {
+                return new Long(t2.startTimestamp).compareTo(t1.startTimestamp);
+            }
+        };
+        Collections.sort(testRuns, comparator);
+        generateToTBreakdown();
+
+        headerRow = new String[testRuns.size() + 1];
+        headerRow[0] = StringUtils.join(HEADER_NAMES, "<br>");
+
+        summaryGrid = new String[SUMMARY_NAMES.length][testRuns.size() + 1];
+        for (int i = 0; i < SUMMARY_NAMES.length; i++) {
+            summaryGrid[i][0] = "<b>" + SUMMARY_NAMES[i] + "</b>";
+        }
+
+        timeGrid = new String[TIME_INFO_NAMES.length][testRuns.size() + 1];
+        for (int i = 0; i < TIME_INFO_NAMES.length; i++) {
+            timeGrid[i][0] = "<b>" + TIME_INFO_NAMES[i] + "</b>";
+        }
+
+        durationGrid = new String[DURATION_INFO_NAMES.length][testRuns.size() + 1];
+        for (int i = 0; i < DURATION_INFO_NAMES.length; i++) {
+            durationGrid[i][0] = "<b>" + DURATION_INFO_NAMES[i] + "</b>";
+        }
+
+        resultsGrid = new String[testCaseNameMap.size()][testRuns.size() + 1];
+        // first column for results grid
+        for (String testCaseName : testCaseNameMap.keySet()) {
+            resultsGrid[testCaseNameMap.get(testCaseName)][0] = testCaseName;
+        }
+
+        // Iterate through the test runs
+        for (int col = 0; col < testRuns.size(); col++) {
+            TestRunEntity testRun = testRuns.get(col);
+
+            // Process the device information
+            List<DeviceInfoEntity> devices = deviceInfoMap.get(testRun.key);
+            List<String> buildIdList = new ArrayList<>();
+            List<String> buildAliasList = new ArrayList<>();
+            List<String> buildFlavorList = new ArrayList<>();
+            List<String> productVariantList = new ArrayList<>();
+            List<String> abiInfoList = new ArrayList<>();
+            for (DeviceInfoEntity deviceInfoEntity : devices) {
+                buildAliasList.add(deviceInfoEntity.branch);
+                buildFlavorList.add(deviceInfoEntity.buildFlavor);
+                productVariantList.add(deviceInfoEntity.product);
+                buildIdList.add(deviceInfoEntity.buildId);
+                String abi = "";
+                String abiName = deviceInfoEntity.abiName;
+                String abiBitness = deviceInfoEntity.abiBitness;
+                if (abiName.length() > 0) {
+                    abi += abiName;
+                    if (abiBitness.length() > 0) {
+                        abi += " (" + abiBitness + " bit)";
+                    }
+                }
+                abiInfoList.add(abi);
+            }
+
+            String buildAlias = StringUtils.join(buildAliasList, ",");
+            String buildFlavor = StringUtils.join(buildFlavorList, ",");
+            String productVariant = StringUtils.join(productVariantList, ",");
+            String buildIds = StringUtils.join(buildIdList, ",");
+            String abiInfo = StringUtils.join(abiInfoList, ",");
+            String vtsBuildId = testRun.testBuildId;
+
+            int totalCount = 0;
+            int passCount = (int) testRun.passCount;
+            int nonpassCount = (int) testRun.failCount;
+            TestCaseResult aggregateStatus = TestCaseResult.UNKNOWN_RESULT;
+            long totalLineCount = testRun.totalLineCount;
+            long coveredLineCount = testRun.coveredLineCount;
+
+            // Process test case results
+            for (TestCaseRunEntity testCaseEntity : testCaseRunMap.get(testRun.key)) {
+                // Update the aggregated test run status
+                totalCount += testCaseEntity.testCases.size();
+                for (TestCase testCase : testCaseEntity.testCases) {
+                    int result = testCase.result;
+                    String name = testCase.name;
+                    if (result == TestCaseResult.TEST_CASE_RESULT_PASS.getNumber()) {
+                        if (aggregateStatus == TestCaseResult.UNKNOWN_RESULT) {
+                            aggregateStatus = TestCaseResult.TEST_CASE_RESULT_PASS;
+                        }
+                    } else if (result != TestCaseResult.TEST_CASE_RESULT_SKIP.getNumber()) {
+                        aggregateStatus = TestCaseResult.TEST_CASE_RESULT_FAIL;
+                    }
+
+                    String systraceUrl = null;
+
+                    if (testCaseEntity.getSystraceUrl() != null) {
+                        String url = testCaseEntity.getSystraceUrl();
+                        LinkDisplay validatedLink = UrlUtil.processUrl(url);
+                        if (validatedLink != null) {
+                            systraceUrl = validatedLink.url;
+                        } else {
+                            logger.log(Level.WARNING, "Invalid systrace URL : " + url);
+                        }
+                    }
+
+                    int index = testCaseNameMap.get(name);
+                    String classNames = "test-case-status ";
+                    String glyph = "";
+                    TestCaseResult testCaseResult = TestCaseResult.valueOf(result);
+                    if (testCaseResult != null)
+                        classNames += testCaseResult.toString();
+                    else
+                        classNames += TestCaseResult.UNKNOWN_RESULT.toString();
+
+                    if (systraceUrl != null) {
+                        classNames += " width-1";
+                        glyph += "<a href=\"" + systraceUrl + "\" "
+                                + "class=\"waves-effect waves-light btn red right inline-btn\">"
+                                + "<i class=\"material-icons inline-icon\">info_outline</i></a>";
+                    }
+                    resultsGrid[index][col + 1] =
+                            "<div class=\"" + classNames + "\">&nbsp;</div>" + glyph;
+                }
+            }
+            String passInfo;
+            try {
+                double passPct =
+                        Math.round((100 * passCount / (passCount + nonpassCount)) * 100f) / 100f;
+                passInfo = Double.toString(passPct) + "%";
+            } catch (ArithmeticException e) {
+                passInfo = " - ";
+            }
+
+            // Process coverage metadata
+            String coverageInfo;
+            String coveragePctInfo;
+            try {
+                double coveragePct =
+                        Math.round((100 * coveredLineCount / totalLineCount) * 100f) / 100f;
+                coveragePctInfo = Double.toString(coveragePct) + "%"
+                        + "<a href=\"/show_coverage?testName=" + testName + "&startTime="
+                        + testRun.startTimestamp
+                        + "\" class=\"waves-effect waves-light btn red right inline-btn\">"
+                        + "<i class=\"material-icons inline-icon\">menu</i></a>";
+                coverageInfo = coveredLineCount + "/" + totalLineCount;
+            } catch (ArithmeticException e) {
+                coveragePctInfo = " - ";
+                coverageInfo = " - ";
+            }
+
+            // Process log information
+            String logSummary = " - ";
+            List<String[]> logEntries = new ArrayList<>();
+            logInfoMap.put(Integer.toString(col), logEntries);
+
+            if (testRun.logLinks != null) {
+                for (String rawUrl : testRun.logLinks) {
+                    LinkDisplay validatedLink = UrlUtil.processUrl(rawUrl);
+                    if (validatedLink == null) {
+                        logger.log(Level.WARNING, "Invalid logging URL : " + rawUrl);
+                        continue;
+                    }
+                    String[] logInfo = new String[] {
+                            validatedLink.name,
+                            validatedLink.url // TODO: process the name from the URL
+                    };
+                    logEntries.add(logInfo);
+                }
+            }
+            if (logEntries.size() > 0) {
+                logSummary = Integer.toString(logEntries.size());
+                logSummary += "<i class=\"waves-effect waves-light btn red right inline-btn"
+                        + " info-btn material-icons inline-icon\""
+                        + " data-col=\"" + Integer.toString(col) + "\""
+                        + ">launch</i>";
+            }
+
+            String icon = "<div class='status-icon " + aggregateStatus.toString() + "'>&nbsp</div>";
+            String hostname = testRun.hostName;
+
+            // Populate the header row
+            headerRow[col + 1] = "<span class='valign-wrapper'><b>" + buildIds + "</b>" + icon
+                    + "</span>" + buildAlias + "<br>" + buildFlavor + "<br>" + productVariant
+                    + "<br>" + abiInfo + "<br>" + vtsBuildId + "<br>" + hostname;
+
+            // Populate the test summary grid
+            summaryGrid[0][col + 1] = Integer.toString(totalCount);
+            summaryGrid[1][col + 1] = Integer.toString(passCount);
+            summaryGrid[2][col + 1] = Integer.toString(nonpassCount);
+            summaryGrid[3][col + 1] = passInfo;
+            summaryGrid[4][col + 1] = coverageInfo;
+            summaryGrid[5][col + 1] = coveragePctInfo;
+            summaryGrid[6][col + 1] = logSummary;
+
+            // Populate the test time info grid
+            timeGrid[0][col + 1] = Long.toString(testRun.startTimestamp);
+            timeGrid[1][col + 1] = Long.toString(testRun.endTimestamp);
+
+            // Populate the test duration info grid
+            durationGrid[0][col + 1] = Long.toString(testRun.endTimestamp - testRun.startTimestamp);
+        }
+
+        profilingPointNames =
+                profilingPointNameSet.toArray(new String[profilingPointNameSet.size()]);
+        Arrays.sort(profilingPointNames);
+    }
+}
diff --git a/src/main/java/com/android/vts/util/TestRunDetails.java b/src/main/java/com/android/vts/util/TestRunDetails.java
new file mode 100644
index 0000000..41e20c4
--- /dev/null
+++ b/src/main/java/com/android/vts/util/TestRunDetails.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.TestCaseRunEntity;
+import com.android.vts.entity.TestCaseRunEntity.TestCase;
+import com.android.vts.proto.VtsReportMessage.TestCaseResult;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Helper object for describing test results data. */
+public class TestRunDetails {
+    private static final String NAME_KEY = "name";
+    private static final String DATA_KEY = "data";
+
+    private class ResultColumn {
+        private final String name;
+        private final List<String> testCases;
+
+        public ResultColumn(String name) {
+            this.name = name;
+            this.testCases = new ArrayList<>();
+        }
+
+        public void add(String testCase) {
+            this.testCases.add(testCase);
+        }
+
+        public int size() {
+            return this.testCases.size();
+        }
+
+        public JsonObject toJson() {
+            Collections.sort(testCases);
+            JsonObject json = new JsonObject();
+            json.add(NAME_KEY, new JsonPrimitive(name));
+            json.add(DATA_KEY, new Gson().toJsonTree(testCases));
+            return json;
+        }
+    }
+
+    private final ResultColumn[] columns;
+    public final int[] resultCounts = new int[TestCaseResult.values().length];
+
+    public TestRunDetails() {
+        columns = new ResultColumn[TestCaseResult.values().length];
+        for (TestCaseResult r : TestCaseResult.values()) {
+            columns[r.getNumber()] = new ResultColumn(r.name());
+        }
+    }
+
+    /**
+     * Add a test case entity to the details object.
+     * @param testCaseEntity The TestCaseRunEntity object storing test case results.
+     */
+    public void addTestCase(TestCaseRunEntity testCaseEntity) {
+        for (TestCase testCase : testCaseEntity.testCases) {
+            int result = testCase.result;
+            if (result > resultCounts.length)
+                continue;
+            ++resultCounts[result];
+            ResultColumn column = columns[result];
+            column.add(testCase.name);
+        }
+    }
+
+    /**
+     * Serializes the test run details to json format.
+     *
+     * @return A JsonObject object representing the details object.
+     */
+    public JsonElement toJson() {
+        List<JsonObject> jsonColumns = new ArrayList<>();
+        for (ResultColumn column : columns) {
+            if (column.size() > 0) {
+                jsonColumns.add(column.toJson());
+            }
+        }
+        return new Gson().toJsonTree(jsonColumns);
+    }
+}
diff --git a/src/main/java/com/android/vts/util/TestRunMetadata.java b/src/main/java/com/android/vts/util/TestRunMetadata.java
new file mode 100644
index 0000000..0cc375c
--- /dev/null
+++ b/src/main/java/com/android/vts/util/TestRunMetadata.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.TestRunEntity;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.Query;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+
+/** Helper object for describing test results data. */
+public class TestRunMetadata {
+    private static final String TEST_RUN = "testRun";
+    private static final String TEST_DETAILS = "testDetails";
+    private static final String DEVICE_INFO = "deviceInfo";
+    private static final String ABI_INFO = "abiInfo";
+
+    public final TestRunEntity testRun;
+
+    private String deviceInfo;
+    private String abiInfo;
+    private TestRunDetails details;
+
+    /**
+     * Create a test metadata object.
+     *
+     * @param testName The name of the test.
+     */
+    public TestRunMetadata(String testName, TestRunEntity testRun) {
+        this.testRun = testRun;
+        this.deviceInfo = "";
+        this.abiInfo = "";
+        this.details = null;
+        processDeviceInfo();
+    }
+
+    /**
+     * Get device information for the test run and add it to the metadata message.
+     */
+    private void processDeviceInfo() {
+        DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+        Query deviceInfoQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRun.key);
+        List<String> deviceInfoList = new ArrayList<>();
+        List<String> abiInfoList = new ArrayList<>();
+
+        for (Entity device : datastore.prepare(deviceInfoQuery).asIterable()) {
+            DeviceInfoEntity deviceInfoEntity = DeviceInfoEntity.fromEntity(device);
+            if (deviceInfoEntity == null) {
+                continue;
+            }
+            String abi = "";
+            String abiName = deviceInfoEntity.abiName;
+            String abiBitness = deviceInfoEntity.abiBitness;
+            if (abiName.length() > 0) {
+                abi += abiName;
+                if (abiBitness.length() > 0) {
+                    abi += " (" + abiBitness + " bit)";
+                }
+            }
+            abiInfoList.add(abi);
+            deviceInfoList.add(deviceInfoEntity.branch + "/" + deviceInfoEntity.buildFlavor + " ("
+                    + deviceInfoEntity.buildId + ")");
+        }
+        this.abiInfo = StringUtils.join(abiInfoList, ", ");
+        this.deviceInfo = StringUtils.join(deviceInfoList, ", ");
+    }
+
+    /**
+     * Get the device info string in the test metadata.
+     * @return The string descriptor of the devices used in the test run.
+     */
+    public String getDeviceInfo() {
+        return this.deviceInfo;
+    }
+
+    /**
+     * Get the test run details (e.g. test case information) for the test run.
+     * @return The TestRunDetails object stored in the metadata, or null if not set.
+     */
+    public TestRunDetails getDetails() {
+        return this.details;
+    }
+
+    /**
+     * Add test case details to the metadata object.
+     *
+     * Used for prefetching details on initial page load.
+     * @param details The TestRunDetails object storing test case results for the test run.
+     */
+    public void addDetails(TestRunDetails details) {
+        this.details = details;
+    }
+
+    /**
+     * Serializes the test run metadata to json format.
+     *
+     * @return A JsonElement object representing the details object.
+     */
+    public JsonObject toJson() {
+        JsonObject json = new JsonObject();
+        json.add(DEVICE_INFO, new JsonPrimitive(this.deviceInfo));
+        json.add(ABI_INFO, new JsonPrimitive(this.abiInfo));
+        json.add(TEST_RUN, this.testRun.toJson());
+        if (this.details != null) {
+            json.add(TEST_DETAILS, this.details.toJson());
+        }
+        return json;
+    }
+}
diff --git a/src/main/java/com/android/vts/util/UrlUtil.java b/src/main/java/com/android/vts/util/UrlUtil.java
new file mode 100644
index 0000000..03c23eb
--- /dev/null
+++ b/src/main/java/com/android/vts/util/UrlUtil.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2017 Google Inc. All Rights Reserved.
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.vts.util;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/** UrlUtil, a helper class for formatting and validating URLs. */
+public class UrlUtil {
+    private static final String HTTPS = "https";
+
+    public static class LinkDisplay {
+        public final String name;
+        public final String url;
+
+        /**
+         * Create a link display object.
+         *
+         * @param uri The hyperlink URI.
+         */
+        public LinkDisplay(URI uri) {
+            this.url = uri.toString();
+
+            // Parse the name from the URI path
+            int lastSeparator = uri.getPath().lastIndexOf('/');
+            if (lastSeparator < 0) {
+                this.name = uri.toString();
+            } else {
+                this.name = uri.getPath().substring(lastSeparator + 1);
+            }
+        }
+    }
+
+    /**
+     * Validates and formats a URL.
+     *
+     * <p>Ensures that the protocol is HTTPs and the URL is properly formatted. This avoids link
+     * issues in the UI and possible vulnerabilities due to embedded JS in URLs.
+     *
+     * @param urlString The url string to validate.
+     * @returns The validated LinkDisplay object.
+     */
+    public static LinkDisplay processUrl(String urlString) {
+        try {
+            URL url = new URL(urlString);
+            String scheme = url.getProtocol();
+            String userInfo = url.getUserInfo();
+            String host = url.getHost();
+            int port = url.getPort();
+            String path = url.getPath();
+            String query = url.getQuery();
+            String fragment = url.getRef();
+            if (!url.getProtocol().equals(HTTPS))
+                throw new MalformedURLException();
+            URI uri = new URI(scheme, userInfo, host, port, path, query, fragment);
+            return new LinkDisplay(uri);
+        } catch (MalformedURLException | URISyntaxException e) {
+            return null;
+        }
+    }
+}
diff --git a/src/main/webapp/WEB-INF/appengine-web.xml b/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 0000000..6c7632c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2016 Google Inc.
+  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.
+-->
+
+<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
+  <application>s~google.com:android-vts-staging</application>
+  <version>4</version>
+  <threadsafe>true</threadsafe>
+
+  <system-properties>
+    <property name="EMAIL_DOMAIN" value="${appengine.emailDomain}" />
+    <property name="SENDER_EMAIL" value="${appengine.senderEmail}" />
+    <property name="DEFAULT_EMAIL" value="${appengine.defaultEmail}" />
+    <property name="SERVICE_CLIENT_ID" value="${appengine.serviceClientID}" />
+    <property name="CLIENT_ID" value="${appengine.clientID}" />
+    <property name="GERRIT_URI" value="${gerrit.uri}" />
+    <property name="GERRIT_SCOPE" value="${gerrit.scope}" />
+    <property name="ANALYTICS_ID" value="${analytics.id}" />
+  </system-properties>
+
+</appengine-web-app>
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/cron.xml b/src/main/webapp/WEB-INF/cron.xml
new file mode 100644
index 0000000..d8dc49e
--- /dev/null
+++ b/src/main/webapp/WEB-INF/cron.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright 2016 Google Inc. All Rights Reserved.
+
+ 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.
+-->
+<cronentries>
+  <cron>
+    <url>/cron/vts_alert_job</url>
+    <description>Send test failure emails.</description>
+    <schedule>every 2 minutes</schedule>
+  </cron>
+  <cron>
+    <url>/cron/vts_performance_job</url>
+    <description>Send daily performance digests.</description>
+    <schedule>every day 07:30</schedule>
+    <timezone>America/Los_Angeles</timezone>
+  </cron>
+</cronentries>
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/datastore-indexes.xml b/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 0000000..01983f5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2017 Google Inc.
+  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.
+-->
+
+<datastore-indexes autoGenerate="true">
+
+  <datastore-index kind="TestPlanRun" ancestor="true" source="manual">
+    <property name="type" direction="asc"/>
+    <property name="__key__" direction="desc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestPlanRun" ancestor="true" source="manual">
+    <property name="type" direction="asc"/>
+    <property name="__key__" direction="asc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestPlanRun" ancestor="true" source="manual">
+    <property name="__key__" direction="desc"/>
+  </datastore-index>
+
+  <datastore-index kind="Test" ancestor="false" source="manual">
+    <property name="failCount" direction="asc"/>
+    <property name="passCount" direction="asc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="true" source="manual">
+    <property name="type" direction="asc"/>
+    <property name="__key__" direction="desc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="true" source="manual">
+    <property name="type" direction="asc"/>
+    <property name="__key__" direction="asc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="true" source="manual">
+    <property name="__key__" direction="desc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="true" source="manual">
+    <property name="hasCoverage" direction="asc"/>
+    <property name="__key__" direction="desc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="false" source="manual">
+    <property name="testName" direction="asc"/>
+    <property name="startTimestamp" direction="asc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="false" source="manual">
+    <property name="testName" direction="asc"/>
+    <property name="startTimestamp" direction="desc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="false" source="manual">
+    <property name="testName" direction="asc"/>
+    <property name="type" direction="asc"/>
+    <property name="startTimestamp" direction="asc"/>
+  </datastore-index>
+
+  <datastore-index kind="TestRun" ancestor="false" source="manual">
+    <property name="testName" direction="asc"/>
+    <property name="type" direction="asc"/>
+    <property name="startTimestamp" direction="desc"/>
+  </datastore-index>
+
+</datastore-indexes>
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp b/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
new file mode 100644
index 0000000..3b72110
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/dashboard_main.jsp
@@ -0,0 +1,177 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <link rel='stylesheet' href='/css/dashboard_main.css'>
+  <%@ include file='header.jsp' %>
+  <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
+  <body>
+    <script>
+        var allTests = ${allTestsJson};
+        var testSet = new Set(allTests);
+        var subscriptionMap = ${subscriptionMapJson};
+
+        var addFavorite = function() {
+            if ($(this).hasClass('disabled')) {
+                return;
+            }
+            var test = $('#input-box').val();
+            if (!testSet.has(test) || test in subscriptionMap) {
+                return;
+            }
+            $('#add-button').addClass('disabled');
+            $.post('/api/favorites/' + test).then(function(data) {
+                if (!data.key) {
+                    return;
+                }
+                subscriptionMap[test] = data.key;
+                var wrapper = $('<div></div>');
+                var a = $('<a></a>')
+                    .attr('href', '/show_table?testName=' + test);
+                var div = $('<div class="col s11 card hoverable option"></div>');
+                div.addClass('valign-wrapper waves-effect');
+                div.appendTo(a);
+                var span = $('<span class="entry valign"></span>').text(test);
+                span.appendTo(div);
+                a.appendTo(wrapper);
+                var clear = $('<a class="col s1 btn-flat center"></a>');
+                clear.addClass('clear-button');
+                clear.append('<i class="material-icons">clear</i>');
+                clear.attr('test', test);
+                clear.appendTo(wrapper);
+                clear.click(removeFavorite);
+                wrapper.prependTo('#options').hide()
+                                          .slideDown(150);
+                $('#input-box').val(null);
+                Materialize.updateTextFields();
+            }).always(function() {
+                $('#add-button').removeClass('disabled');
+            });
+        }
+
+        var removeFavorite = function() {
+            var self = $(this);
+            if (self.hasClass('disabled')) {
+                return;
+            }
+            var test = self.attr('test');
+            if (!(test in subscriptionMap)) {
+                return;
+            }
+            self.addClass('disabled');
+            $.ajax({
+                url: '/api/favorites/' + subscriptionMap[test],
+                type: 'DELETE'
+            }).always(function() {
+                self.removeClass('disabled');
+            }).then(function() {
+                delete subscriptionMap[test];
+                self.parent().slideUp(150, function() {
+                    self.remove();
+                });
+            });
+        }
+
+        $.widget('custom.sizedAutocomplete', $.ui.autocomplete, {
+            _resizeMenu: function() {
+                this.menu.element.outerWidth($('#input-box').width());
+            }
+        });
+
+        $(function() {
+            $('#input-box').sizedAutocomplete({
+                source: allTests,
+                classes: {
+                    'ui-autocomplete': 'card'
+                }
+            });
+
+            $('#input-box').keyup(function(event) {
+                if (event.keyCode == 13) {  // return button
+                    $('#add-button').click();
+                }
+            });
+
+            $('.clear-button').click(removeFavorite);
+            $('#add-button').click(addFavorite);
+        });
+    </script>
+    <div class='container'>
+      <c:choose>
+        <c:when test='${not empty error}'>
+          <div id='error-container' class='row card'>
+            <div class='col s12 center-align'>
+              <h5>${error}</h5>
+            </div>
+          </div>
+        </c:when>
+        <c:otherwise>
+          <c:set var='width' value='${showAll ? 12 : 11}' />
+          <c:if test='${not showAll}'>
+            <div class='row'>
+              <div class='input-field col s8'>
+                <input type='text' id='input-box'></input>
+                <label for='input-box'>Search for tests to add to favorites</label>
+              </div>
+              <div id='add-button-wrapper' class='col s1 valign-wrapper'>
+                <a id='add-button' class='btn-floating btn waves-effect waves-light red valign'><i class='material-icons'>add</i></a>
+              </div>
+            </div>
+          </c:if>
+          <div class='row'>
+            <div class='col s12'>
+              <h4 id='section-header'>${headerLabel}</h4>
+            </div>
+          </div>
+          <div class='row' id='options'>
+            <c:forEach items='${testNames}' var='test'>
+              <div>
+                <a href='/show_table?testName=${test.name}'>
+                  <div class='col s${width} card hoverable option valign-wrapper waves-effect'>
+                    <span class='entry valign'>${test.name}
+                      <c:if test='${test.failCount >= 0 && test.passCount >= 0}'>
+                        <c:set var='color' value='${test.failCount > 0 ? "red" : (test.passCount > 0 ? "green" : "grey")}' />
+                        <span class='indicator center ${color}'>
+                          ${test.passCount} / ${test.passCount + test.failCount}
+                        </span>
+                      </c:if>
+                    </span>
+                  </div>
+                </a>
+                <c:if test='${not showAll}'>
+                  <a class='col s1 btn-flat center clear-button' test='${test.name}'>
+                    <i class='material-icons'>clear</i>
+                  </a>
+                </c:if>
+              </div>
+            </c:forEach>
+          </div>
+        </c:otherwise>
+      </c:choose>
+    </div>
+    <c:if test='${empty error}'>
+      <div class='center'>
+        <a href='${buttonLink}' id='show-button' class='btn waves-effect red'>${buttonLabel}
+          <i id='show-button-arrow' class='material-icons right'>${buttonIcon}</i>
+        </a>
+      </div>
+    </c:if>
+    <%@ include file='footer.jsp' %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/footer.jsp b/src/main/webapp/WEB-INF/jsp/footer.jsp
new file mode 100644
index 0000000..ed2d950
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/footer.jsp
@@ -0,0 +1,25 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+<footer class='page-footer'>
+  <div class='footer-copyright'>
+    <div class='container'>
+      © 2017 - The Android Open Source Project
+    </div>
+  </div>
+</footer>
diff --git a/src/main/webapp/WEB-INF/jsp/header.jsp b/src/main/webapp/WEB-INF/jsp/header.jsp
new file mode 100644
index 0000000..77ec600
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/header.jsp
@@ -0,0 +1,70 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<head>
+  <link rel='stylesheet' href='https://www.gstatic.com/external_hosted/materialize/all_styles-bundle.css'>
+  <link rel='icon' href='https://www.gstatic.com/images/branding/googleg/1x/googleg_standard_color_32dp.png' sizes='32x32'>
+  <link rel='stylesheet' href='https://fonts.googleapis.com/icon?family=Material+Icons'>
+  <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700'>
+  <link rel='stylesheet' href='/css/navbar.css'>
+  <link rel='stylesheet' href='/css/common.css'>
+  <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js'></script>
+  <script src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/materialize/materialize.min.js'></script>
+  <script type='text/javascript'>
+    if (${analyticsID}) {
+        // Autogenerated from Google Analytics
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+        })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+        ga('create', ${analyticsID}, 'auto');
+        ga('send', 'pageview');
+    }
+  </script>
+  <title>VTS Dashboard</title>
+</head>
+<body>
+  <nav id='navbar'>
+    <div class='nav-wrapper'>
+      <a href='#' class='brand-logo center'>VTS Dashboard</a>
+      <ul class='nav-list'>
+        <c:forEach items='${navbarLinks}' var='link' varStatus='loop'>
+          <li class='${loop.index == activeIndex ? "active" : ""}'>
+            <a class='nav-list-item' href='${link.url}'>${link.name}</a>
+          </li>
+        </c:forEach>
+      </ul>
+      <ul class='right'><li>
+        <a id='dropdown-button' class='dropdown-button btn red lighten-3' href='#' data-activates='dropdown'>
+          ${email}
+        </a>
+      </li></ul>
+      <ul id='dropdown' class='dropdown-content'>
+        <li><a href='${logoutURL}'>Log out</a></li>
+      </ul>
+      <c:if test='${breadcrumbLinks != null}'>
+        <div id='nav-sublist'>
+          <c:forEach items='${breadcrumbLinks}' var='link'>
+            <a href='${link.url}' class='nav-sublist-item breadcrumb'>${link.name}</a>
+          </c:forEach>
+        </div>
+      </c:if>
+    </div>
+  </nav>
+</body>
diff --git a/src/main/webapp/WEB-INF/jsp/show_coverage.jsp b/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
new file mode 100644
index 0000000..790590d
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_coverage.jsp
@@ -0,0 +1,171 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link rel="stylesheet" href="/css/show_coverage.css">
+  <script src="https://apis.google.com/js/api.js" type="text/javascript"></script>
+  <body>
+    <script type="text/javascript">
+        var coverageVectors = ${coverageVectors};
+        $(document).ready(function() {
+            // Initialize AJAX for CORS
+            $.ajaxSetup({
+                xhrFields : {
+                    withCredentials: true
+                }
+            });
+
+            // Initialize auth2 client and scope for requests to Gerrit
+            gapi.load('auth2', function() {
+                var auth2 = gapi.auth2.init({
+                    client_id: ${clientId},
+                    scope: ${gerritScope}
+                });
+                auth2.then(displayEntries);
+            });
+        });
+
+        /* Open a window to Gerrit so that user can login.
+           Minimize the previously clicked entry.
+        */
+        var gerritLogin = function(element) {
+            window.open(${gerritURI}, "Ratting", "toolbar=0,status=0");
+            element.click();
+        }
+
+        /* Loads source code for a particular entry and displays it with
+           coverage information as the accordion entry expands.
+        */
+        var onClick = function() {
+            // Remove source code from the accordion entry that was open before
+            var self = $(this);
+            var prev = self.parent().siblings('li.active');
+            if (prev.length > 0) {
+                prev.find('.table-container').empty();
+            }
+            var url = self.parent().attr('url');
+            var i = self.parent().attr('index');
+            var container = self.parent().find('.table-container');
+            container.html('<div class="center-align">Loading...</div>');
+            if (self.parent().hasClass('active')) {
+                // Remove the code from display
+                container.empty();
+            } else {
+                /* Fetch and display the code.
+                   Note: a coverageVector may be shorter than sourceContents due
+                   to non-executable (i.e. comments or language-specific syntax)
+                   lines in the code. Trailing source lines that have no
+                   coverage information are assumed to be non-executable.
+                */
+                $.ajax({
+                    url: url,
+                    dataType: 'text'
+                }).promise().done(function(src) {
+                    src = atob(src);
+                    if (!src) return;
+                    srcLines = src.split('\n');
+                    covered = 0;
+                    total = 0;
+                    var table = $('<table class="table"></table>');
+                    var rows = srcLines.forEach(function(line, j) {
+                        var count = coverageVectors[i][j];
+                        var row = $('<tr></tr>');
+                        if (typeof count == 'undefined' || count < 0) {
+                            count = "--";
+                        } else if (count == 0) {
+                            row.addClass('uncovered');
+                            total += 1;
+                        } else {
+                            row.addClass('covered');
+                            total += 1;
+                        }
+                        row.append('<td class="count">' + String(count) + '</td>');
+                        row.append('<td class="line_no">' + String(j+1) + '</td>');
+                        code = $('<td class="code"></td>');
+                        code.text(String(line));
+                        code.appendTo(row);
+                        row.appendTo(table);
+                    });
+                    container.empty();
+                    container.append(table);
+                }).fail(function(error) {
+                    if (error.status == 0) {  // origin error, refresh cookie
+                        container.empty();
+                        container.html('<div class="center-align">' +
+                                       '<span class="login-button">' +
+                                       'Click to authorize Gerrit access' +
+                                       '</span></div>');
+                        container.find('.login-button').click(function() {
+                            gerritLogin(self);
+                        });
+                    } else {
+                        container.html('<div class="center-align">' +
+                                       'Not found.</div>');
+                    }
+                });
+            }
+        }
+
+        /* Appends a row to the display with test name and aggregated coverage
+           information. On expansion, source code is loaded with coverage
+           highlighted by calling 'onClick'.
+        */
+        var displayEntries = function() {
+            var sourceFilenames = ${sourceFiles};
+            var sectionMap = ${sectionMap};
+            var gerritURI = ${gerritURI};
+            var projects = ${projects};
+            var commits = ${commits};
+            var indicators = ${indicators};
+            Object.keys(sectionMap).forEach(function(section) {
+                var indices = sectionMap[section];
+                var html = String();
+                indices.forEach(function(i) {
+                    var url = gerritURI + '/projects/' +
+                              encodeURIComponent(projects[i]) + '/commits/' +
+                              encodeURIComponent(commits[i]) + '/files/' +
+                              encodeURIComponent(sourceFilenames[i]) +
+                              '/content';
+                    html += '<li url="' + url + '" index="' + i + '">' +
+                            '<div class="collapsible-header">' +
+                            '<i class="material-icons">library_books</i>' +
+                            sourceFilenames[i] + indicators[i] + '</div>';
+                    html += '<div class="collapsible-body row">' +
+                            '<div class="html-container">' +
+                            '<div class="table-container"></div>' +
+                            '</div></div></li>';
+                });
+                if (html) {
+                    html = '<h4 class="section-title"><b>Coverage:</b> ' +
+                           section + '</h4><ul class="collapsible popout" ' +
+                           'data-collapsible="accordion">' + html + '</ul>';
+                    $('#coverage-container').append(html);
+                }
+            });
+            $('.collapsible.popout').collapsible({
+               accordion : true
+            }).find('.collapsible-header').click(onClick);
+        }
+    </script>
+    <div id='coverage-container' class='wide container'>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp b/src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp
new file mode 100644
index 0000000..b06e9df
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_coverage_overview.jsp
@@ -0,0 +1,168 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <!-- <link rel='stylesheet' href='/css/dashboard_main.css'> -->
+  <%@ include file='header.jsp' %>
+  <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+  <link type='text/css' href='/css/test_results.css' rel='stylesheet'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script src='js/time.js'></script>
+  <script src='js/test_results.js'></script>
+  <script type='text/javascript'>
+      google.charts.load('current', {'packages':['table', 'corechart']});
+      google.charts.setOnLoadCallback(drawStatsChart);
+      google.charts.setOnLoadCallback(drawCoverageCharts);
+
+      $(document).ready(function() {
+          $('#test-results-container').showTests(${testRuns}, true);
+      });
+
+      // draw test statistics chart
+      function drawStatsChart() {
+          var testStats = ${testStats};
+          if (testStats.length < 1) {
+              return;
+          }
+          var resultNames = ${resultNamesJson};
+          var rows = resultNames.map(function(res, i) {
+              nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+                         .trim().toLowerCase();
+              return [nickname, parseInt(testStats[i])];
+          });
+          rows.unshift(['Result', 'Count']);
+
+          // Get CSS color definitions (or default to white)
+          var colors = resultNames.map(function(res) {
+              return $('.' + res).css('background-color') || 'white';
+          });
+
+          var data = google.visualization.arrayToDataTable(rows);
+          var options = {
+              is3D: false,
+              colors: colors,
+              fontName: 'Roboto',
+              fontSize: '14px',
+              legend: {position: 'labeled'},
+              tooltip: {showColorCode: true, ignoreBounds: false},
+              chartArea: {height: '80%', width: '90%'},
+              pieHole: 0.4
+          };
+
+          var chart = new google.visualization.PieChart(document.getElementById('pie-chart-stats'));
+          chart.draw(data, options);
+      }
+
+      // draw the coverage pie charts
+      function drawCoverageCharts() {
+          var coveredLines = ${coveredLines};
+          var uncoveredLines = ${uncoveredLines};
+          var rows = [
+              ["Result", "Count"],
+              ["Covered Lines", coveredLines],
+              ["Uncovered Lines", uncoveredLines]
+          ];
+
+          // Get CSS color definitions (or default to white)
+          var colors = [
+              $('.TEST_CASE_RESULT_PASS').css('background-color') || 'white',
+              $('.TEST_CASE_RESULT_FAIL').css('background-color') || 'white'
+          ]
+
+          var data = google.visualization.arrayToDataTable(rows);
+
+
+          var optionsRaw = {
+              is3D: false,
+              colors: colors,
+              fontName: 'Roboto',
+              fontSize: '14px',
+              pieSliceText: 'value',
+              legend: {position: 'bottom'},
+              chartArea: {height: '80%', width: '90%'},
+              tooltip: {showColorCode: true, ignoreBounds: false, text: 'value'},
+              pieHole: 0.4
+          };
+
+          var optionsNormalized = {
+              is3D: false,
+              colors: colors,
+              fontName: 'Roboto',
+              fontSize: '14px',
+              legend: {position: 'bottom'},
+              tooltip: {showColorCode: true, ignoreBounds: false, text: 'percentage'},
+              chartArea: {height: '80%', width: '90%'},
+              pieHole: 0.4
+          };
+
+          var chart = new google.visualization.PieChart(document.getElementById('pie-chart-coverage-raw'));
+          chart.draw(data, optionsRaw);
+
+          chart = new google.visualization.PieChart(document.getElementById('pie-chart-coverage-normalized'));
+          chart.draw(data, optionsNormalized);
+      }
+
+  </script>
+
+  <body>
+    <div class='wide container'>
+      <div class='row'>
+        <div class='col s12'>
+          <div class='col s12 card center-align'>
+            <div id='legend-wrapper'>
+              <c:forEach items='${resultNames}' var='res'>
+                <div class='center-align legend-entry'>
+                  <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+                  <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+                  <label for='${res}'>${nickname}</label>
+                  <div id='${res}' class='${res} legend-bubble'></div>
+                </div>
+              </c:forEach>
+            </div>
+          </div>
+        </div>
+        <div class='col s4 valign-wrapper'>
+          <!-- pie chart -->
+          <div class='pie-chart-wrapper col s12 valign center-align card'>
+            <h6 class='pie-chart-title'>Test Statistics</h6>
+            <div id='pie-chart-stats' class='pie-chart-div'></div>
+          </div>
+        </div>
+        <div class='col s4 valign-wrapper'>
+          <!-- pie chart -->
+          <div class='pie-chart-wrapper col s12 valign center-align card'>
+            <h6 class='pie-chart-title'>Coverage (Raw)</h6>
+            <div id='pie-chart-coverage-raw' class='pie-chart-div'></div>
+          </div>
+        </div>
+        <div class='col s4 valign-wrapper'>
+          <!-- pie chart -->
+          <div class='pie-chart-wrapper col s12 valign center-align card'>
+            <h6 class='pie-chart-title'>Coverage (Normalized)</h6>
+            <div id='pie-chart-coverage-normalized' class='pie-chart-div'></div>
+          </div>
+        </div>
+      </div>
+      <div class='col s12' id='test-results-container'>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_graph.jsp b/src/main/webapp/WEB-INF/jsp/show_graph.jsp
new file mode 100644
index 0000000..7111f58
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_graph.jsp
@@ -0,0 +1,292 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
+  <link type='text/css' href='/css/show_graph.css' rel='stylesheet'>
+  <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'></script>
+  <body>
+    <script type='text/javascript'>
+        google.charts.load('current', {packages:['corechart', 'table', 'line']});
+        google.charts.setOnLoadCallback(drawAllGraphs);
+
+        ONE_DAY = 86400000000;
+        MICRO_PER_MILLI = 1000;
+        N_BUCKETS = 200;
+
+        var graphs = ${graphs};
+
+        $(function() {
+            $('select').material_select();
+            var date = $('#date').datepicker({
+                showAnim: 'slideDown',
+                maxDate: new Date()
+            });
+            date.datepicker('setDate', new Date(${endTime} / MICRO_PER_MILLI));
+            $('#load').click(load);
+            $('#outlier-select').change(drawAllGraphs);
+        });
+
+        // Draw all graphs.
+        function drawAllGraphs() {
+            $('#profiling-container').empty();
+            var percentileIndex = Number($('#outlier-select').val());
+
+            // Get histogram extrema
+            var histMin = null;
+            var histMax = null;
+            graphs.forEach(function(g) {
+                if (g.type != 'HISTOGRAM') return;
+                var minVal;
+                var maxVal;
+                if (percentileIndex == -1) {
+                    minVal = g.min;
+                    maxVal = g.max;
+                } else {
+                    minVal = g.percentile_values[percentileIndex];
+                    var endIndex = g.percentiles.length - percentileIndex - 1
+                    maxVal = g.percentile_values[endIndex];
+                }
+                if (!histMin || minVal < histMin) histMin = minVal;
+                if (!histMax || maxVal > histMax) histMax = maxVal;
+            });
+
+            graphs.forEach(function(graph) {
+                if (graph.type == 'LINE_GRAPH') drawLineGraph(graph);
+                else if (graph.type == 'HISTOGRAM')
+                    drawHistogram(graph, histMin, histMax);
+            });
+        }
+
+       /**
+        * Draw a line graph.
+        *
+        * Args:
+        *     lineGraph: a JSON object containing the following fields:
+        *                - name: the name of the graph
+        *                - values: an array of numbers
+        *                - ticks: an array of strings to use as x-axis labels
+        *                - ids: an array of string labels for each point (e.g. the
+        *                       build info for the run that produced the point)
+        *                - x_label: the string label for the x axis
+        *                - y_label: the string label for the y axis
+        */
+        function drawLineGraph(lineGraph) {
+            if (!lineGraph.ticks || lineGraph.ticks.length < 1) {
+                return;
+            }
+            var title = 'Performance';
+            if (lineGraph.name) title += ' (' + lineGraph.name + ')';
+            lineGraph.ticks.forEach(function (label, i) {
+                lineGraph.values[i].unshift(label);
+            });
+            var data = new google.visualization.DataTable();
+            data.addColumn('string', lineGraph.x_label);
+            lineGraph.ids.forEach(function(id) {
+                data.addColumn('number', id);
+            });
+            data.addRows(lineGraph.values);
+            var options = {
+              chart: {
+                  title: title,
+                  subtitle: lineGraph.y_label
+              },
+              legend: { position: 'none' }
+            };
+            var container = $('<div class="row card center-align col s12 graph-wrapper"></div>');
+            container.appendTo('#profiling-container');
+            var chartDiv = $('<div class="col s12 graph"></div>');
+            chartDiv.appendTo(container);
+            var chart = new google.charts.Line(chartDiv[0]);
+            chart.draw(data, options);
+        }
+
+       /**
+        * Draw a histogram.
+        *
+        * Args:
+        *     hist: a JSON object containing the following fields:
+        *           - name: the name of the graph
+        *           - values: an array of numbers
+        *           - ids: an array of string labels for each point (e.g. the
+        *                  build info for the run that produced the point)
+        *           - x_label: the string label for the x axis
+        *           - y_label: the string label for the y axis
+        *     min: the minimum value to display
+        *     max: the maximum value to display
+        */
+        function drawHistogram(hist, min, max) {
+            if (!hist.values || hist.values.length == 0) return;
+            var title = 'Performance';
+            if (hist.name) title += ' (' + hist.name + ')';
+            var values = hist.values;
+            var histogramData = values.reduce(function(result, d, i) {
+                if (d <= max && d >= min) result.push([hist.ids[i], d]);
+                return result;
+            }, []);
+
+            var data = google.visualization.arrayToDataTable(histogramData, true);
+            var bucketSize = (max - min) / N_BUCKETS;
+
+            var options = {
+                title: title,
+                titleTextStyle: {
+                    color: '#757575',
+                    fontSize: 16,
+                    bold: false
+                },
+                legend: { position: 'none' },
+                colors: ['#4285F4'],
+                fontName: 'Roboto',
+                vAxis:{
+                    title: hist.y_label,
+                    titleTextStyle: {
+                        color: '#424242',
+                        fontSize: 12,
+                        italic: false
+                    },
+                    textStyle: {
+                        fontSize: 12,
+                        color: '#757575'
+                    },
+                },
+                hAxis: {
+                    title: hist.x_label,
+                    textStyle: {
+                        fontSize: 12,
+                        color: '#757575'
+                    },
+                    titleTextStyle: {
+                        color: '#424242',
+                        fontSize: 12,
+                        italic: false
+                    }
+                },
+                bar: { gap: 0 },
+                histogram: {
+                    minValue: min,
+                    maxValue: max,
+                    maxNumBuckets: N_BUCKETS,
+                    bucketSize: bucketSize
+                },
+                chartArea: {
+                    width: '100%',
+                    top: 40,
+                    left: 60,
+                    height: 375
+                }
+            };
+            var container = $('<div class="row card col s12 graph-wrapper"></div>');
+            container.appendTo('#profiling-container');
+
+            var chartDiv = $('<div class="col s12 graph"></div>');
+            chartDiv.appendTo(container);
+            var chart = new google.visualization.Histogram(chartDiv[0]);
+            chart.draw(data, options);
+
+            var tableDiv = $('<div class="col s12"></div>');
+            tableDiv.appendTo(container);
+
+            var tableHtml = '<table class="percentile-table"><thead><tr>';
+            hist.percentiles.forEach(function(p) {
+                tableHtml += '<th data-field="id">' + p + '%</th>';
+            });
+            tableHtml += '</tr></thead><tbody><tr>';
+            hist.percentile_values.forEach(function(v) {
+                tableHtml += '<td>' + v + '</td>';
+            });
+            tableHtml += '</tbody></table>';
+            $(tableHtml).appendTo(tableDiv);
+        }
+
+        // Reload the page.
+        function load() {
+            var endTime = $('#date').datepicker('getDate').getTime();
+            endTime = endTime + (ONE_DAY / MICRO_PER_MILLI) - 1;
+            var filterVal = $('#outlier-select').val();
+            var ctx = '${pageContext.request.contextPath}';
+            var link = ctx + '/show_graph?profilingPoint=${profilingPointName}' +
+                '&testName=${testName}' +
+                '&endTime=' + (endTime * MICRO_PER_MILLI) +
+                '&filterVal=' + filterVal;
+            if ($('#device-select').prop('selectedIndex') > 1) {
+                link += '&device=' + $('#device-select').val();
+            }
+            window.open(link,'_self');
+        }
+    </script>
+    <div id='download' class='fixed-action-btn'>
+      <a id='b' class='btn-floating btn-large red waves-effect waves-light'>
+        <i class='large material-icons'>file_download</i>
+      </a>
+    </div>
+    <div class='container wide'>
+      <div class='row card'>
+        <div id='header-container' class='valign-wrapper col s12'>
+          <div class='col s3 valign'>
+            <h5>Profiling Point:</h5>
+          </div>
+          <div class='col s9 right-align valign'>
+            <h5 class='profiling-name truncate'>${profilingPointName}</h5>
+          </div>
+        </div>
+        <div id='date-container' class='col s12'>
+          <c:set var='offset' value='${showFilterDropdown ? 0 : 2}' />
+          <c:if test='${showFilterDropdown}'>
+            <div id='outlier-select-wrapper' class='col s2'>
+              <select id='outlier-select'>
+                <option value='-1' ${filterVal eq -1 ? 'selected' : ''}>Show outliers</option>
+                <option value='0' ${filterVal eq 0 ? 'selected' : ''}>Filter outliers (1%)</option>
+                <option value='1' ${filterVal eq 1 ? 'selected' : ''}>Filter outliers (2%)</option>
+                <option value='2' ${filterVal eq 2 ? 'selected' : ''}>Filter outliers (5%)</option>
+              </select>
+            </div>
+          </c:if>
+          <div id='device-select-wrapper' class='input-field col s5 m3 offset-m${offset + 4} offset-s${offset}'>
+            <select id='device-select'>
+              <option value='' disabled>Select device</option>
+              <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
+              <c:forEach items='${devices}' var='device' varStatus='loop'>
+                <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
+              </c:forEach>
+            </select>
+          </div>
+          <input type='text' id='date' name='date' class='col s4 m2'>
+          <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
+            <i class='medium material-icons'>cached</i>
+          </a>
+        </div>
+      </div>
+      <div id='profiling-container'>
+      </div>
+      <c:if test='${not empty error}'>
+        <div id='error-container' class='row card'>
+          <div class='col s10 offset-s1 center-align'>
+            <!-- Error in case of profiling data is missing -->
+            <h5>${error}</h5>
+          </div>
+        </div>
+      </c:if>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp b/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp
new file mode 100644
index 0000000..224d847
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_performance_digest.jsp
@@ -0,0 +1,100 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/datepicker.css' rel='stylesheet'>
+  <link type='text/css' href='/css/show_performance_digest.css' rel='stylesheet'>
+  <link rel='stylesheet' href='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.css'>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
+  <body>
+    <script type='text/javascript'>
+        ONE_DAY = 86400000000;
+        MICRO_PER_MILLI = 1000;
+
+        function load() {
+            var time = $('#date').datepicker('getDate').getTime() - 1;
+            time = time * MICRO_PER_MILLI + ONE_DAY;  // end of day
+            var ctx = '${pageContext.request.contextPath}';
+            var link = ctx + '/show_performance_digest?profilingPoint=${profilingPointName}' +
+                '&testName=${testName}' +
+                '&startTime=' + time;
+            if ($('#device-select').prop('selectedIndex') > 1) {
+                link += '&device=' + $('#device-select').val();
+            }
+            window.open(link,'_self');
+        }
+
+        $(function() {
+            var date = $('#date').datepicker({
+                showAnim: "slideDown",
+                maxDate: new Date()
+            });
+            date.datepicker('setDate', new Date(${startTime} / MICRO_PER_MILLI));
+            $('#load').click(load);
+
+            $('.date-label').each(function(i) {
+                var label = $(this);
+                label.html(moment(parseInt(label.html())).format('M/D/YY'));
+            });
+            $('select').material_select();
+        });
+    </script>
+    <div class='wide container'>
+      <div class='row card'>
+        <div id='header-container' class='col s12'>
+          <div class='col s12'>
+            <h4>Daily Performance Digest</h4>
+          </div>
+          <div id='device-select-wrapper' class='input-field col s6 m3 offset-m6'>
+            <select id='device-select'>
+              <option value='' disabled>Select device</option>
+              <option value='0' ${empty selectedDevice ? 'selected' : ''}>All Devices</option>
+              <c:forEach items='${devices}' var='device' varStatus='loop'>
+                <option value=${device} ${selectedDevice eq device ? 'selected' : ''}>${device}</option>
+              </c:forEach>
+            </select>
+          </div>
+          <input type='text' id='date' name='date' class='col s5 m2'>
+          <a id='load' class='btn-floating btn-medium red right waves-effect waves-light'>
+            <i class='medium material-icons'>cached</i>
+          </a>
+        </div>
+      </div>
+      <div class='row'>
+        <c:forEach items='${tables}' var='table' varStatus='loop'>
+          <div class='col s12 card summary'>
+            <div class='col s3 valign'>
+              <h5>Profiling Point:</h5>
+            </div>
+            <div class='col s9 right-align valign'>
+              <h5 class="profiling-name truncate">${tableTitles[loop.index]}</h5>
+            </div>
+            ${table}
+            <span class='profiling-subtitle'>
+              ${tableSubtitles[loop.index]}
+            </span>
+          </div>
+        </c:forEach>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_plan_release.jsp b/src/main/webapp/WEB-INF/jsp/show_plan_release.jsp
new file mode 100644
index 0000000..b202d04
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_plan_release.jsp
@@ -0,0 +1,109 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link rel='stylesheet' href='/css/show_plan_release.css'>
+  <link rel='stylesheet' href='/css/plan_runs.css'>
+  <link rel='stylesheet' href='/css/search_header.css'>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script src='js/time.js'></script>
+  <script src='js/plan_runs.js'></script>
+  <script src='js/search_header.js'></script>
+  <script type='text/javascript'>
+      var search;
+      $(document).ready(function() {
+          // disable buttons on load
+          if (!${hasNewer}) {
+            $('#newer-button').toggleClass('disabled');
+          }
+          if (!${hasOlder}) {
+            $('#older-button').toggleClass('disabled');
+          }
+
+          $('#newer-button').click(prev);
+          $('#older-button').click(next);
+          search = $('#filter-bar').createSearchHeader('Plan: ', '${plan}', refresh);
+          search.addFilter('Branch', 'branch', {
+            corpus: ${branches}
+          }, ${branch});
+          search.addFilter('Device', 'device', {
+            corpus: ${devices}
+          }, ${device});
+          search.addFilter('Device Build ID', 'deviceBuildId', {}, ${deviceBuildId});
+          search.addRunTypeCheckboxes(${showPresubmit}, ${showPostsubmit});
+          search.display();
+          $('#release-container').showPlanRuns(${planRuns});
+      });
+
+      // view older data
+      function next() {
+        if($(this).hasClass('disabled')) return;
+        var endTime = ${startTime};
+        var link = '${pageContext.request.contextPath}' +
+            '/show_plan_release?plan=${plan}&endTime=' + endTime +
+            search.args();
+        if (${unfiltered}) {
+          link += '&unfiltered=';
+        }
+        window.open(link,'_self');
+      }
+
+      // view newer data
+      function prev() {
+        if($(this).hasClass('disabled')) return;
+        var startTime = ${endTime};
+        var link = '${pageContext.request.contextPath}' +
+            '/show_plan_release?plan=${plan}&startTime=' + startTime +
+            search.args();
+        if (${unfiltered}) {
+          link += '&unfiltered=';
+        }
+        window.open(link,'_self');
+      }
+
+      // refresh the page to see the runs matching the specified filter
+      function refresh() {
+        var link = '${pageContext.request.contextPath}' +
+            '/show_plan_release?plan=${plan}' + search.args();
+        if (${unfiltered}) {
+          link += '&unfiltered=';
+        }
+        window.open(link,'_self');
+      }
+  </script>
+
+  <body>
+    <div class='wide container'>
+      <div id='filter-bar'></div>
+      <div class='row' id='release-container'></div>
+      <div id='newer-wrapper' class='page-button-wrapper fixed-action-btn'>
+        <a id='newer-button' class='btn-floating btn red waves-effect'>
+          <i class='large material-icons'>keyboard_arrow_left</i>
+        </a>
+      </div>
+      <div id='older-wrapper' class='page-button-wrapper fixed-action-btn'>
+        <a id='older-button' class='btn-floating btn red waves-effect'>
+          <i class='large material-icons'>keyboard_arrow_right</i>
+        </a>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_plan_run.jsp b/src/main/webapp/WEB-INF/jsp/show_plan_run.jsp
new file mode 100644
index 0000000..491703a
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_plan_run.jsp
@@ -0,0 +1,131 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+  <link type='text/css' href='/css/test_results.css' rel='stylesheet'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script src='js/time.js'></script>
+  <script src='js/test_results.js'></script>
+  <script type='text/javascript'>
+      google.charts.load('current', {'packages':['table', 'corechart']});
+      google.charts.setOnLoadCallback(drawPieChart);
+
+      $(document).ready(function() {
+          $('#test-results-container').showTests(${testRuns}, true);
+      });
+
+      // to draw pie chart
+      function drawPieChart() {
+          var topBuildResultCounts = ${topBuildResultCounts};
+          if (topBuildResultCounts.length < 1) {
+              return;
+          }
+          var resultNames = ${resultNamesJson};
+          var rows = resultNames.map(function(res, i) {
+              nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+                         .trim().toLowerCase();
+              return [nickname, parseInt(topBuildResultCounts[i])];
+          });
+          rows.unshift(['Result', 'Count']);
+
+          // Get CSS color definitions (or default to white)
+          var colors = resultNames.map(function(res) {
+              return $('.' + res).css('background-color') || 'white';
+          });
+
+          var data = google.visualization.arrayToDataTable(rows);
+          var options = {
+              is3D: false,
+              colors: colors,
+              fontName: 'Roboto',
+              fontSize: '14px',
+              legend: 'none',
+              tooltip: {showColorCode: true, ignoreBounds: true},
+              chartArea: {height: '90%'}
+          };
+
+          var chart = new google.visualization.PieChart(document.getElementById('pie-chart-div'));
+          chart.draw(data, options);
+      }
+  </script>
+
+  <body>
+    <div class='wide container'>
+      <div class='row'>
+        <div class='col s7'>
+          <div class='col s12 card center-align'>
+            <div id='legend-wrapper'>
+              <c:forEach items='${resultNames}' var='res'>
+                <div class='center-align legend-entry'>
+                  <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+                  <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+                  <label for='${res}'>${nickname}</label>
+                  <div id='${res}' class='${res} legend-bubble'></div>
+                </div>
+              </c:forEach>
+            </div>
+          </div>
+          <div id='profiling-container' class='col s12'>
+            <c:choose>
+              <c:when test='${empty profilingPointNames}'>
+                <div id='error-div' class='center-align card'><h5>${error}</h5></div>
+              </c:when>
+              <c:otherwise>
+                <ul id='profiling-body' class='collapsible' data-collapsible='accordion'>
+                  <li>
+                    <div class='collapsible-header'><i class='material-icons'>timeline</i>Profiling Graphs</div>
+                    <div class='collapsible-body'>
+                      <ul id='profiling-list' class='collection'>
+                        <c:forEach items='${profilingPointNames}' var='pt'>
+                          <c:set var='profPointArgs' value='testName=${testName}&profilingPoint=${pt}'/>
+                          <c:set var='timeArgs' value='endTime=${endTime}'/>
+                          <a href='/show_graph?${profPointArgs}&${timeArgs}'
+                             class='collection-item profiling-point-name'>${pt}
+                          </a>
+                        </c:forEach>
+                      </ul>
+                    </div>
+                  </li>
+                  <li>
+                    <a class='collapsible-link' href='/show_performance_digest?testName=${testName}'>
+                      <div class='collapsible-header'><i class='material-icons'>toc</i>Performance Digest</div>
+                    </a>
+                  </li>
+                </ul>
+              </c:otherwise>
+            </c:choose>
+          </div>
+        </div>
+        <div class='col s5 valign-wrapper'>
+          <!-- pie chart -->
+          <div id='pie-chart-wrapper' class='col s12 valign center-align card'>
+            <div id='pie-chart-div'></div>
+          </div>
+        </div>
+      </div>
+
+      <div class='col s12' id='test-results-container'>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_release.jsp b/src/main/webapp/WEB-INF/jsp/show_release.jsp
new file mode 100644
index 0000000..b3da353
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_release.jsp
@@ -0,0 +1,45 @@
+<%--
+  ~ Copyright (c) 2017 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <link rel='stylesheet' href='/css/show_release.css'>
+  <%@ include file='header.jsp' %>
+  <script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.0/jquery-ui.min.js'></script>
+  <body>
+    <div class='container'>
+      <div class='row'>
+        <div class='col s12'>
+          <h4 id='section-header'>Test Plans</h4>
+        </div>
+      </div>
+      <div class='row' id='options'>
+        <c:forEach items='${planNames}' var='plan'>
+          <div>
+            <a href='/show_plan_release?plan=${plan}'>
+              <div class='col s12 card hoverable option valign-wrapper waves-effect'>
+                <span class='entry valign'>${plan}</span>
+              </div>
+            </a>
+          </div>
+        </c:forEach>
+      </div>
+    </div>
+    <%@ include file='footer.jsp' %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_table.jsp b/src/main/webapp/WEB-INF/jsp/show_table.jsp
new file mode 100644
index 0000000..be71ee8
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_table.jsp
@@ -0,0 +1,340 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/show_table.css' rel='stylesheet'>
+  <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+  <link type='text/css' href='/css/search_header.css' rel='stylesheet'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script src='js/search_header.js'></script>
+  <script type='text/javascript'>
+      google.charts.load('current', {'packages':['table', 'corechart']});
+      google.charts.setOnLoadCallback(drawGridTable);
+      google.charts.setOnLoadCallback(activateLogLinks);
+      google.charts.setOnLoadCallback(drawPieChart);
+      google.charts.setOnLoadCallback(function() {
+          $('.gradient').removeClass('gradient');
+      });
+
+      var search;
+
+      $(document).ready(function() {
+          search = $('#filter-bar').createSearchHeader('Module: ', '${testName}', refresh);
+          search.addFilter('Branch', 'branch', {
+            corpus: ${branches}
+          }, ${branch});
+          search.addFilter('Device', 'device', {
+            corpus: ${devices}
+          }, ${device});
+          search.addFilter('Device Build ID', 'deviceBuildId', {}, ${deviceBuildId});
+          search.addFilter('Test Build ID', 'testBuildId', {}, ${testBuildId});
+          search.addFilter('Host', 'hostname', {}, ${hostname});
+          search.addFilter('Passing Count', 'passing', {
+            type: 'number',
+            width: 's2'
+          }, ${passing});
+          search.addFilter('Non-Passing Count', 'nonpassing', {
+            type: 'number',
+            width: 's2'
+          }, ${nonpassing});
+          search.addRunTypeCheckboxes(${showPresubmit}, ${showPostsubmit});
+          search.display();
+
+          // disable buttons on load
+          if (!${hasNewer}) {
+              $('#newer-button').toggleClass('disabled');
+          }
+          if (!${hasOlder}) {
+              $('#older-button').toggleClass('disabled');
+          }
+          $('#treeLink').click(function() {
+              window.open('/show_tree?testName=${testName}', '_self');
+          });
+          $('#newer-button').click(prev);
+          $('#older-button').click(next);
+      });
+
+      // Actives the log links to display the log info modal when clicked.
+      function activateLogLinks() {
+          $('.info-btn').click(function(e) {
+              showLog(${logInfoMap}[$(this).data('col')]);
+          });
+      }
+
+      /** Displays a modal window with the specified log entries.
+       *
+       * @param logEntries Array of string arrays. Each entry in the outer array
+       *                   must contain (1) name string, and (2) url string.
+       */
+      function showLog(logEntries) {
+          if (!logEntries || logEntries.length == 0) return;
+
+          var logList = $('<ul class="collection"></ul>');
+          var entries = logEntries.reduce(function(acc, entry) {
+              if (!entry || entry.length == 0) return acc;
+              var link = '<a href="' + entry[1] + '"';
+              link += 'class="collection-item">' + entry[0] + '</li>';
+              return acc + link;
+          }, '');
+          logList.html(entries);
+          var infoContainer = $('#info-modal>.modal-content>.info-container');
+          infoContainer.empty();
+          logList.appendTo(infoContainer);
+          $('#info-modal').openModal();
+      }
+
+      // refresh the page to see the selected test types (pre-/post-submit)
+      function refresh() {
+          if($(this).hasClass('disabled')) return;
+          var link = '${pageContext.request.contextPath}' +
+              '/show_table?testName=${testName}' + search.args();
+          if (${unfiltered}) {
+              link += '&unfiltered=';
+          }
+          window.open(link,'_self');
+      }
+
+      // view older data
+      function next() {
+          if($(this).hasClass('disabled')) return;
+          var endTime = ${startTime};
+          var link = '${pageContext.request.contextPath}' +
+              '/show_table?testName=${testName}&endTime=' + endTime +
+              search.args();
+          if (${unfiltered}) {
+              link += '&unfiltered=';
+          }
+          window.open(link,'_self');
+      }
+
+      // view newer data
+      function prev() {
+          if($(this).hasClass('disabled')) return;
+          var startTime = ${endTime};
+          var link = '${pageContext.request.contextPath}' +
+              '/show_table?testName=${testName}&startTime=' + startTime +
+              search.args();
+          if (${unfiltered}) {
+              link += '&unfiltered=';
+          }
+          window.open(link,'_self');
+        }
+
+      // to draw pie chart
+      function drawPieChart() {
+          var topBuildResultCounts = ${topBuildResultCounts};
+          if (topBuildResultCounts.length < 1) {
+              return;
+          }
+          var resultNames = ${resultNamesJson};
+          var rows = resultNames.map(function(res, i) {
+              nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+                         .trim().toLowerCase();
+              return [nickname, parseInt(topBuildResultCounts[i])];
+          });
+          rows.unshift(['Result', 'Count']);
+
+          // Get CSS color definitions (or default to white)
+          var colors = resultNames.map(function(res) {
+              return $('.' + res).css('background-color') || 'white';
+          });
+
+          var data = google.visualization.arrayToDataTable(rows);
+          var options = {
+              is3D: false,
+              colors: colors,
+              fontName: 'Roboto',
+              fontSize: '14px',
+              legend: 'none',
+              tooltip: {showColorCode: true, ignoreBounds: true},
+              chartArea: {height: '90%'}
+          };
+
+          var chart = new google.visualization.PieChart(document.getElementById('pie-chart-div'));
+          chart.draw(data, options);
+      }
+
+      // table for grid data
+      function drawGridTable() {
+          var data = new google.visualization.DataTable();
+
+          // Add column headers.
+          headerRow = ${headerRow};
+          headerRow.forEach(function(d, i) {
+              var classNames = 'table-header-content';
+              if (i == 0) classNames += ' table-header-legend';
+              data.addColumn('string', '<span class="' + classNames + '">' +
+                             d + '</span>');
+          });
+
+          var timeGrid = ${timeGrid};
+          var durationGrid = ${durationGrid};
+          var summaryGrid = ${summaryGrid};
+          var resultsGrid = ${resultsGrid};
+
+          // Format time grid to a formatted date
+          timeGrid = timeGrid.map(function(row) {
+              return row.map(function(cell, j) {
+                  if (j == 0) return cell;
+                  var time = moment(cell/1000);
+                  // If today, don't display the date
+                  if (time.isSame(moment(), 'd')) {
+                      return time.format('H:mm:ssZZ');
+                  } else {
+                      return time.format('M/D/YY H:mm:ssZZ');
+                  }
+              });
+          });
+
+          // Format duration grid to HH:mm:ss.SSS
+          durationGrid = durationGrid.map(function(row) {
+              return row.map(function(cell, j) {
+                  if (j == 0) return cell;
+                  return moment.utc(cell/1000).format("HH:mm:ss.SSS");
+              });
+          });
+
+          // add rows to the data.
+          data.addRows(timeGrid);
+          data.addRows(durationGrid);
+          data.addRows(summaryGrid);
+          data.addRows(resultsGrid);
+
+          var table = new google.visualization.Table(document.getElementById('grid-table-div'));
+          var classNames = {
+              headerRow : 'table-header',
+              headerCell : 'table-header-cell'
+          };
+          var options = {
+              showRowNumber: false,
+              alternatingRowStyle: true,
+              allowHtml: true,
+              frozenColumns: 1,
+              cssClassNames: classNames,
+              sort: 'disable'
+          };
+          table.draw(data, options);
+      }
+  </script>
+
+  <body>
+    <div class='wide container'>
+      <div class='row'>
+        <div class='col s12'>
+          <div class='card'>
+            <ul class='tabs'>
+              <li class='tab col s6'><a class='active'>Table</a></li>
+              <li class='tab col s6' id='treeLink'><a>Tree</a></li>
+            </ul>
+          </div>
+          <div id='filter-bar'></div>
+        </div>
+        <div class='col s7'>
+          <div class='col s12 card center-align'>
+            <div id='legend-wrapper'>
+              <c:forEach items='${resultNames}' var='res'>
+                <div class='center-align legend-entry'>
+                  <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+                  <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+                  <label for='${res}'>${nickname}</label>
+                  <div id='${res}' class='${res} legend-bubble'></div>
+                </div>
+              </c:forEach>
+            </div>
+          </div>
+          <div id='profiling-container' class='col s12'>
+            <c:choose>
+              <c:when test='${empty profilingPointNames}'>
+                <div id='error-div' class='center-align card'><h5>${error}</h5></div>
+              </c:when>
+              <c:otherwise>
+                <ul id='profiling-body' class='collapsible' data-collapsible='accordion'>
+                  <li>
+                    <div class='collapsible-header'><i class='material-icons'>timeline</i>Profiling Graphs</div>
+                    <div class='collapsible-body'>
+                      <ul id='profiling-list' class='collection'>
+                        <c:forEach items='${profilingPointNames}' var='pt'>
+                          <c:set var='profPointArgs' value='testName=${testName}&profilingPoint=${pt}'/>
+                          <c:set var='timeArgs' value='endTime=${endTime}'/>
+                          <a href='/show_graph?${profPointArgs}&${timeArgs}'
+                             class='collection-item profiling-point-name'>${pt}
+                          </a>
+                        </c:forEach>
+                      </ul>
+                    </div>
+                  </li>
+                  <li>
+                    <a class='collapsible-link' href='/show_performance_digest?testName=${testName}'>
+                      <div class='collapsible-header'><i class='material-icons'>toc</i>Performance Digest</div>
+                    </a>
+                  </li>
+                </ul>
+              </c:otherwise>
+            </c:choose>
+          </div>
+        </div>
+        <div class='col s5 valign-wrapper'>
+          <!-- pie chart -->
+          <div id='pie-chart-wrapper' class='col s12 valign center-align card'>
+            <h6 class='pie-chart-title'>Test Status for Device Build ID: ${topBuildId}</h6>
+            <div id='pie-chart-div'></div>
+          </div>
+        </div>
+      </div>
+
+      <div class='col s12'>
+        <div id='chart-holder' class='col s12 card'>
+          <!-- Grid tables-->
+          <div id='grid-table-div'></div>
+        </div>
+      </div>
+      <div id='newer-wrapper' class='page-button-wrapper fixed-action-btn'>
+        <a id='newer-button' class='btn-floating btn red waves-effect'>
+          <i class='large material-icons'>keyboard_arrow_left</i>
+        </a>
+      </div>
+      <div id='older-wrapper' class='page-button-wrapper fixed-action-btn'>
+        <a id='older-button' class='btn-floating btn red waves-effect'>
+          <i class='large material-icons'>keyboard_arrow_right</i>
+        </a>
+      </div>
+    </div>
+    <div id="help-modal" class="modal">
+      <div class="modal-content">
+        <h4>${searchHelpHeader}</h4>
+        <p>${searchHelpBody}</p>
+      </div>
+      <div class="modal-footer">
+        <a href="#!" class="modal-action modal-close waves-effect btn-flat">Close</a>
+      </div>
+    </div>
+    <div id="info-modal" class="modal">
+      <div class="modal-content">
+        <h4>Logs</h4>
+        <div class="info-container"></div>
+      </div>
+      <div class="modal-footer">
+        <a href="#!" class="modal-action modal-close waves-effect btn-flat">Close</a>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/jsp/show_tree.jsp b/src/main/webapp/WEB-INF/jsp/show_tree.jsp
new file mode 100644
index 0000000..8d237a3
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/show_tree.jsp
@@ -0,0 +1,225 @@
+<%--
+  ~ Copyright (c) 2016 Google Inc. All Rights Reserved.
+  ~
+  ~ 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.
+  --%>
+<%@ page contentType='text/html;charset=UTF-8' language='java' %>
+<%@ taglib prefix='fn' uri='http://java.sun.com/jsp/jstl/functions' %>
+<%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%>
+
+<html>
+  <%@ include file="header.jsp" %>
+  <link type='text/css' href='/css/show_test_runs_common.css' rel='stylesheet'>
+  <link type='text/css' href='/css/test_results.css' rel='stylesheet'>
+  <link rel='stylesheet' href='/css/search_header.css'>
+  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
+  <script src='https://www.gstatic.com/external_hosted/moment/min/moment-with-locales.min.js'></script>
+  <script src='js/search_header.js'></script>
+  <script src='js/time.js'></script>
+  <script src='js/test_results.js'></script>
+  <script type='text/javascript'>
+      google.charts.load('current', {'packages':['table', 'corechart']});
+      google.charts.setOnLoadCallback(drawPieChart);
+
+      var search;
+
+      $(document).ready(function() {
+          search = $('#filter-bar').createSearchHeader('Module: ', '${testName}', refresh);
+          search.addFilter('Branch', 'branch', {
+            corpus: ${branches}
+          }, ${branch});
+          search.addFilter('Device', 'device', {
+            corpus: ${devices}
+          }, ${device});
+          search.addFilter('Device Build ID', 'deviceBuildId', {}, ${deviceBuildId});
+          search.addFilter('Test Build ID', 'testBuildId', {}, ${testBuildId});
+          search.addFilter('Host', 'hostname', {}, ${hostname});
+          search.addFilter('Passing Count', 'passing', {
+            type: 'number',
+            width: 's2'
+          }, ${passing});
+          search.addFilter('Non-Passing Count', 'nonpassing', {
+            type: 'number',
+            width: 's2'
+          }, ${nonpassing});
+          search.addRunTypeCheckboxes(${showPresubmit}, ${showPostsubmit});
+          search.display();
+
+          // disable buttons on load
+          if (!${hasNewer}) {
+              $('#newer-button').toggleClass('disabled');
+          }
+          if (!${hasOlder}) {
+              $('#older-button').toggleClass('disabled');
+          }
+          $('#tableLink').click(function() {
+              window.open('/show_table?testName=${testName}', '_self');
+          });
+          $('#newer-button').click(prev);
+          $('#older-button').click(next);
+          $('#test-results-container').showTests(${testRuns});
+      });
+
+      // refresh the page to see the selected test types (pre-/post-submit)
+      function refresh() {
+          if($(this).hasClass('disabled')) return;
+          var link = '${pageContext.request.contextPath}' +
+              '/show_tree?testName=${testName}' + search.args();
+          if (${unfiltered}) {
+            link += '&unfiltered=';
+          }
+          window.open(link,'_self');
+      }
+
+      // view older data
+      function next() {
+          if($(this).hasClass('disabled')) return;
+          var endTime = ${startTime};
+          var link = '${pageContext.request.contextPath}' +
+              '/show_tree?testName=${testName}&endTime=' + endTime +
+              search.args();
+          if (${unfiltered}) {
+              link += '&unfiltered=';
+          }
+          window.open(link,'_self');
+      }
+
+      // view newer data
+      function prev() {
+          if($(this).hasClass('disabled')) return;
+          var startTime = ${endTime};
+          var link = '${pageContext.request.contextPath}' +
+              '/show_tree?testName=${testName}&startTime=' + startTime +
+              search.args();
+          if (${unfiltered}) {
+              link += '&unfiltered=';
+          }
+          window.open(link,'_self');
+        }
+
+      // to draw pie chart
+      function drawPieChart() {
+          var topBuildResultCounts = ${topBuildResultCounts};
+          if (topBuildResultCounts.length < 1) {
+              return;
+          }
+          var resultNames = ${resultNamesJson};
+          var rows = resultNames.map(function(res, i) {
+              nickname = res.replace('TEST_CASE_RESULT_', '').replace('_', ' ')
+                         .trim().toLowerCase();
+              return [nickname, parseInt(topBuildResultCounts[i])];
+          });
+          rows.unshift(['Result', 'Count']);
+
+          // Get CSS color definitions (or default to white)
+          var colors = resultNames.map(function(res) {
+              return $('.' + res).css('background-color') || 'white';
+          });
+
+          var data = google.visualization.arrayToDataTable(rows);
+          var options = {
+              is3D: false,
+              colors: colors,
+              fontName: 'Roboto',
+              fontSize: '14px',
+              legend: 'none',
+              tooltip: {showColorCode: true, ignoreBounds: true},
+              chartArea: {height: '90%'}
+          };
+
+          var chart = new google.visualization.PieChart(document.getElementById('pie-chart-div'));
+          chart.draw(data, options);
+      }
+  </script>
+
+  <body>
+    <div class='wide container'>
+      <div class='row'>
+        <div class='col s12'>
+          <div class='card'>
+            <ul class='tabs'>
+              <li class='tab col s6' id='tableLink'><a>Table</a></li>
+              <li class='tab col s6'><a class='active'>Tree</a></li>
+            </ul>
+          </div>
+          <div id='filter-bar'></div>
+        </div>
+        <div class='col s7'>
+          <div class='col s12 card center-align'>
+            <div id='legend-wrapper'>
+              <c:forEach items='${resultNames}' var='res'>
+                <div class='center-align legend-entry'>
+                  <c:set var='trimmed' value='${fn:replace(res, "TEST_CASE_RESULT_", "")}'/>
+                  <c:set var='nickname' value='${fn:replace(trimmed, "_", " ")}'/>
+                  <label for='${res}'>${nickname}</label>
+                  <div id='${res}' class='${res} legend-bubble'></div>
+                </div>
+              </c:forEach>
+            </div>
+          </div>
+          <div id='profiling-container' class='col s12'>
+            <c:choose>
+              <c:when test='${empty profilingPointNames}'>
+                <div id='error-div' class='center-align card'><h5>${error}</h5></div>
+              </c:when>
+              <c:otherwise>
+                <ul id='profiling-body' class='collapsible' data-collapsible='accordion'>
+                  <li>
+                    <div class='collapsible-header'><i class='material-icons'>timeline</i>Profiling Graphs</div>
+                    <div class='collapsible-body'>
+                      <ul id='profiling-list' class='collection'>
+                        <c:forEach items='${profilingPointNames}' var='pt'>
+                          <c:set var='profPointArgs' value='testName=${testName}&profilingPoint=${pt}'/>
+                          <c:set var='timeArgs' value='endTime=${endTime}'/>
+                          <a href='/show_graph?${profPointArgs}&${timeArgs}'
+                             class='collection-item profiling-point-name'>${pt}
+                          </a>
+                        </c:forEach>
+                      </ul>
+                    </div>
+                  </li>
+                  <li>
+                    <a class='collapsible-link' href='/show_performance_digest?testName=${testName}'>
+                      <div class='collapsible-header'><i class='material-icons'>toc</i>Performance Digest</div>
+                    </a>
+                  </li>
+                </ul>
+              </c:otherwise>
+            </c:choose>
+          </div>
+        </div>
+        <div class='col s5 valign-wrapper'>
+          <!-- pie chart -->
+          <div id='pie-chart-wrapper' class='col s12 valign center-align card'>
+            <h6 class='pie-chart-title'>${topBuildId}</h6>
+            <div id='pie-chart-div'></div>
+          </div>
+        </div>
+      </div>
+
+      <div class='col s12' id='test-results-container'>
+      </div>
+      <div id='newer-wrapper' class='page-button-wrapper fixed-action-btn'>
+        <a id='newer-button' class='btn-floating btn red waves-effect'>
+          <i class='large material-icons'>keyboard_arrow_left</i>
+        </a>
+      </div>
+      <div id='older-wrapper' class='page-button-wrapper fixed-action-btn'>
+        <a id='older-button' class='btn-floating btn red waves-effect'>
+          <i class='large material-icons'>keyboard_arrow_right</i>
+        </a>
+      </div>
+    </div>
+    <%@ include file="footer.jsp" %>
+  </body>
+</html>
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..e5ed62c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,197 @@
+<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
+<!--
+Copyright 2016 Google Inc. All Rights Reserved.
+
+ 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.
+-->
+
+<servlet>
+  <servlet-name>dashboard_main</servlet-name>
+  <servlet-class>com.android.vts.servlet.DashboardMainServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_release</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowReleaseServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_coverage_overview</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowCoverageOverviewServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_tree</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowTreeServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_table</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowTableServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_graph</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowGraphServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_plan_release</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowPlanReleaseServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_plan_run</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowPlanRunServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_performance_digest</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowPerformanceDigestServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>show_coverage</servlet-name>
+  <servlet-class>com.android.vts.servlet.ShowCoverageServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>datastore</servlet-name>
+  <servlet-class>com.android.vts.api.DatastoreRestServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>test_run</servlet-name>
+  <servlet-class>com.android.vts.api.TestRunRestServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>favorites</servlet-name>
+  <servlet-class>com.android.vts.api.UserFavoriteRestServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>bigtable_legacy</servlet-name>
+  <servlet-class>com.android.vts.api.BigtableLegacyJsonServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>vts_alert_job</servlet-name>
+  <servlet-class>com.android.vts.servlet.VtsAlertJobServlet</servlet-class>
+</servlet>
+
+<servlet>
+  <servlet-name>vts_performance_job</servlet-name>
+  <servlet-class>com.android.vts.servlet.VtsPerformanceJobServlet</servlet-class>
+</servlet>
+
+<servlet-mapping>
+  <servlet-name>dashboard_main</servlet-name>
+  <url-pattern>/</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_release</servlet-name>
+  <url-pattern>/show_release/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_coverage_overview</servlet-name>
+  <url-pattern>/show_coverage_overview/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_tree</servlet-name>
+  <url-pattern>/show_tree/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_table</servlet-name>
+  <url-pattern>/show_table/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_graph</servlet-name>
+  <url-pattern>/show_graph/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_plan_release</servlet-name>
+  <url-pattern>/show_plan_release/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_plan_run</servlet-name>
+  <url-pattern>/show_plan_run/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_performance_digest</servlet-name>
+  <url-pattern>/show_performance_digest/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>show_coverage</servlet-name>
+  <url-pattern>/show_coverage/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>bigtable_legacy</servlet-name>
+  <url-pattern>/api/bigtable/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>datastore</servlet-name>
+  <url-pattern>/api/datastore/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>test_run</servlet-name>
+  <url-pattern>/api/test_run/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>favorites</servlet-name>
+  <url-pattern>/api/favorites/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>vts_alert_job</servlet-name>
+  <url-pattern>/cron/vts_alert_job/*</url-pattern>
+</servlet-mapping>
+
+<servlet-mapping>
+  <servlet-name>vts_performance_job</servlet-name>
+  <url-pattern>/cron/vts_performance_job/*</url-pattern>
+</servlet-mapping>
+
+<security-constraint>
+  <web-resource-collection>
+    <web-resource-name>cron</web-resource-name>
+    <url-pattern>/cron/*</url-pattern>
+  </web-resource-collection>
+  <auth-constraint>
+    <role-name>admin</role-name>
+  </auth-constraint>
+</security-constraint>
+
+<security-constraint>
+  <web-resource-collection>
+    <web-resource-name>all</web-resource-name>
+    <url-pattern>/show_*</url-pattern>
+  </web-resource-collection>
+  <auth-constraint>
+    <role-name>*</role-name>
+  </auth-constraint>
+</security-constraint>
+</web-app>
diff --git a/src/main/webapp/css/common.css b/src/main/webapp/css/common.css
new file mode 100644
index 0000000..974f129
--- /dev/null
+++ b/src/main/webapp/css/common.css
@@ -0,0 +1,25 @@
+/* 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.
+*/
+
+.container {
+    min-height: 80%;
+}
+
+@media only screen and (min-width: 993px) {
+    .wide.container {
+        width: 80%;
+        max-width: 1600px;
+    }
+}
diff --git a/src/main/webapp/css/dashboard_main.css b/src/main/webapp/css/dashboard_main.css
new file mode 100644
index 0000000..e6e899f
--- /dev/null
+++ b/src/main/webapp/css/dashboard_main.css
@@ -0,0 +1,109 @@
+/* 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.
+*/
+
+#edit-button-wrapper {
+    bottom: 25px;
+    right: 25px;
+}
+
+.input-field {
+    margin-bottom: 50px;
+}
+
+#add-button-wrapper {
+    margin-top: 10px;
+    height: 61px;
+}
+
+.btn-flat.clear-button {
+    margin-top: 8px;
+    user-select: none;
+    color: grey;
+}
+
+.row .col.card.option {
+    padding: 6px 15px 6px 15px;
+    margin: 5px 0;
+    border-radius: 25px;
+}
+
+#error-container {
+    padding-top: 50px;
+    padding-bottom: 50px;
+}
+
+.entry {
+    font-size: 20px;
+    font-weight: 300;
+    position: relative;
+}
+
+.indicator {
+    color: white;
+    font-size: 12px;
+    font-weight: bold;
+    padding: 1px 6px;
+    position: absolute;
+    right: 0;
+    min-width: 40px;
+    border-radius: 10px;
+    margin-top: 5px;
+}
+
+#show-button {
+    border-radius: 100px;
+}
+
+#show-button-arrow {
+    margin-left: 3px;
+}
+
+#section-header {
+    cursor: default;
+    user-select: none;
+    color: #ee6e73;
+}
+
+#section-header:after {
+    border: 1px solid #ee6e73;
+    margin-top: 10px;
+    margin-bottom: 0;
+    display: block;
+    content: " ";
+}
+
+.ui-menu {
+    overflow-y: auto;
+    z-index: 100;
+    max-height: 50%;
+}
+
+.ui-menu-item {
+    font-size: 16px;
+    padding: 4px 10px;
+    transition: background-color .25s;
+}
+
+.ui-menu-item:hover {
+    background-color: #e0f2f1;
+}
+
+.ui-menu-item:active {
+    background-color: #b2dfdb;
+}
+
+.ui-helper-hidden-accessible {
+    display: none;
+}
diff --git a/src/main/webapp/css/datepicker.css b/src/main/webapp/css/datepicker.css
new file mode 100644
index 0000000..b98dd80
--- /dev/null
+++ b/src/main/webapp/css/datepicker.css
@@ -0,0 +1,70 @@
+/* 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.
+*/
+.ui-datepicker table {
+    font-family: Roboto !important;
+    font-size: 13px !important;
+    white-space: nowrap !important;
+}
+
+#ui-datepicker-div {
+    font-family: "Roboto", sans-serif;
+    padding: 0;
+}
+
+.ui-corner-all {
+    border-bottom-right-radius: 0 !important;
+    border-bottom-left-radius: 0 !important;
+    border-top-right-radius: 0 !important;
+    border-top-left-radius: 0 !important;
+}
+
+.ui-datepicker td span.ui-state-default, .ui-datepicker td a.ui-state-default {
+    text-align: center;
+    padding: 0.7em 0.4em;
+    border: none;
+    border-radius: 10em;
+    background: none;
+}
+
+.ui-datepicker td span.ui-state-hover, .ui-datepicker td a.ui-state-hover {
+    background: #e0f2f1;
+}
+
+.ui-datepicker td span.ui-state-active, .ui-datepicker td a.ui-state-active {
+    color: white;
+    font-weight: 600;
+    background: #009688;
+}
+
+.ui-datepicker-header.ui-widget-header.ui-helper-clearfix.ui-corner-all {
+    background: #009688;
+    border: none;
+    color: white;
+}
+
+.ui-datepicker-next.ui-state-hover.ui-datepicker-next-hover {
+    right: 2px;
+    top: 2px;
+}
+
+.ui-datepicker-prev.ui-state-hover.ui-datepicker-prev-hover {
+    left: 2px;
+    top: 2px;
+}
+
+.ui-datepicker-next.ui-corner-all.ui-state-hover, .ui-datepicker-prev.ui-corner-all.ui-state-hover {
+    background: none;
+    border: none;
+}
diff --git a/src/main/webapp/css/navbar.css b/src/main/webapp/css/navbar.css
new file mode 100644
index 0000000..9395d1b
--- /dev/null
+++ b/src/main/webapp/css/navbar.css
@@ -0,0 +1,69 @@
+/* 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.
+*/
+nav#navbar {
+    height: auto;
+    margin-bottom: 30px;
+    user-select: none;
+}
+nav#navbar .nav-wrapper {
+    height: auto;
+}
+nav#navbar ul.nav-list {
+    display: inline-block;
+}
+nav#navbar ul li {
+    transition: background-color .3s;
+    float: left;
+    padding: 0;
+}
+nav#navbar ul li.active {
+    background-color: rgba(0,0,0,0.1);
+}
+nav#navbar ul li a:hover, nav#navbar ul li.active {
+    background-color: #ea454b;
+}
+nav#navbar ul a.nav-list-item {
+    font-size: 1.2rem;
+}
+nav#navbar #nav-sublist {
+    line-height: 35px;
+    background: white;
+    padding-left: 15px;
+    width: 100%;
+    display: inline-block;
+    position: relative;
+}
+nav#navbar #nav-sublist .breadcrumb.nav-sublist-item {
+    font-size: 15px;
+    font-weight: 400;
+    color: rgba(238, 110, 115, 0.85);
+    line-height: 35px;
+}
+nav#navbar #nav-sublist .breadcrumb.nav-sublist-item:last-child {
+    color: rgb(238, 110, 115);
+    font-weight: 500;
+}
+nav#navbar #nav-sublist .breadcrumb.nav-sublist-item::before {
+    font-size: 22px;
+    color: rgba(238, 110, 115, 0.85);
+    line-height: 35px;
+    margin: 0 5px;
+}
+nav#navbar #dropdown-button {
+    font-style: italic;
+    color: rgba(255, 255, 255, 0.75);
+    margin-left: 0;
+    margin-top: 0;
+}
diff --git a/src/main/webapp/css/plan_runs.css b/src/main/webapp/css/plan_runs.css
new file mode 100644
index 0000000..3c9eeb9
--- /dev/null
+++ b/src/main/webapp/css/plan_runs.css
@@ -0,0 +1,30 @@
+/* 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.
+*/
+.plan-run-metadata {
+    display: inline-block;
+    font-size: 13px;
+    line-height: 16px;
+    padding: 10px;
+}
+.release-entry {
+    border-radius: 5px 5px 10px 10px;
+}
+.counter {
+    color: white;
+    font-size: 12px;
+    font-weight: bold;
+    display: block;
+    border-radius: 0 0 10px 10px;
+}
\ No newline at end of file
diff --git a/src/main/webapp/css/search_header.css b/src/main/webapp/css/search_header.css
new file mode 100644
index 0000000..90573e1
--- /dev/null
+++ b/src/main/webapp/css/search_header.css
@@ -0,0 +1,80 @@
+/* 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.
+*/
+.row.card.search-bar {
+    margin-left: 0;
+    margin-right: 0;
+}
+.search-bar .header-wrapper {
+    border-bottom: 1px solid rgb(221, 221, 221);;
+}
+.search-bar .section-header {
+    color: rgb(97, 97, 97);
+    font-weight: 300;
+    padding: 15px 20px 15px;
+    margin: 0;
+    cursor: default;
+    user-select: none;
+    display: inline-block;
+}
+.search-bar .section-header b {
+    color: #ee6e73;
+}
+.search-bar .search-icon-wrapper {
+    text-align: center;
+    display: inline-block;
+    position: absolute;
+    right: 0;
+    height: 57px;
+    width: 57px;
+    cursor: pointer;
+    user-select: none;
+}
+.search-bar .search-icon-wrapper i {
+    line-height: 57px;
+    color: rgb(97, 97, 97);
+}
+.search-bar .search-wrapper .refresh-wrapper a {
+    float: right;
+    margin-top: 17px;
+}
+.search-bar .input-field input[type=text].invalid {
+    color: #F44336;
+}
+.search-bar .search-wrapper .run-type-wrapper {
+    margin: 20px 0;
+}
+.search-bar .search-wrapper .run-type-wrapper [type="checkbox"]+label {
+    margin-right: 35px;
+}
+.search-bar-menu  .ui-menu {
+    overflow-y: auto;
+    z-index: 100;
+    max-height: 50%;
+}
+.search-bar-menu  .ui-menu-item {
+    font-size: 16px;
+    padding: 4px 10px;
+    transition: background-color .25s;
+}
+.search-bar-menu  .ui-menu-item:hover {
+    background-color: #e0f2f1;
+}
+
+.search-bar-menu .ui-menu-item:active {
+    background-color: #b2dfdb;
+}
+.search-bar-menu  .ui-helper-hidden-accessible {
+    display: none;
+}
diff --git a/src/main/webapp/css/show_coverage.css b/src/main/webapp/css/show_coverage.css
new file mode 100644
index 0000000..c187fd3
--- /dev/null
+++ b/src/main/webapp/css/show_coverage.css
@@ -0,0 +1,104 @@
+/* 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.
+*/
+
+.collapsible.popout {
+    margin-bottom: 50px;
+}
+
+.table {
+    font-size: 12px;
+    border: none;
+    width: 100%;
+    word-spacing: 4px;
+    font-family: monospace;
+    white-space: PRE;
+    border-collapse: collapse;
+}
+
+.section-title {
+    margin-left: 24px;
+}
+
+.html-container {
+    padding:  25px 25px;
+}
+
+.login-button {
+    border: 1px solid gray;
+    color: gray;
+    border-radius: 15px;
+    padding: 4px 15px;
+    cursor: pointer;
+}
+
+td {
+    padding: 0;
+}
+
+.count {
+    white-space: nowrap;
+    text-align: right;
+    border-right: 1px solid black;
+    padding-right: 5px;
+}
+
+.line_no {
+    padding-left: 35px;
+    white-space: nowrap;
+    padding-right: 5px;
+    border-right: 1px dotted gray;
+}
+
+.code {
+    padding-left: 10px;
+    width: 99%;
+}
+
+.indicator {
+    display: inline-block;
+    width: 50px;
+    margin-top: 12px;
+    line-height: 18px;
+    border-radius: 10px;
+    padding: 2px 5px;
+    text-align: center;
+    font-size: 12px;
+    font-weight: bold;
+    color: white;
+}
+
+.total-count {
+    margin-top: 12px;
+    margin-right: 8px;
+    padding-right: 5px;
+    line-height: 22px;
+    border-right: 1px solid gray;
+    font-size: 12px;
+    font-weight: bold;
+    color: gray;
+}
+
+.uncovered {
+    background-color: LightPink;
+}
+
+.covered {
+    background-color: LightGreen;
+}
+
+.source-name {
+    margin-top: -25px;
+    padding-left: 45px;
+}
diff --git a/src/main/webapp/css/show_graph.css b/src/main/webapp/css/show_graph.css
new file mode 100644
index 0000000..51e4c04
--- /dev/null
+++ b/src/main/webapp/css/show_graph.css
@@ -0,0 +1,70 @@
+/* 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.
+*/
+
+#download {
+    bottom: 25px;
+    right: 25px;
+}
+
+#header-container {
+    padding: 50px;
+    padding-bottom: 30px;
+}
+
+#device-select-wrapper {
+    margin-top: 0;
+}
+
+#date-container {
+    padding: 0 50px;
+    padding-bottom: 25px;
+}
+
+#load {
+    margin-top: 5px;
+}
+
+.profiling-name {
+    font-weight: 200;
+    font-style: italic;
+    font-size: 1.4rem
+}
+
+#error-container {
+    padding-top: 25px;
+    padding-bottom: 25px;
+}
+
+.graph-wrapper {
+    padding: 30px;
+}
+
+.graph {
+    height: 500px;
+    padding-bottom: 30px;
+}
+
+.percentile-table {
+    width: auto;
+    margin: auto;
+    margin-top: 20px;
+}
+
+.percentile-table td, th{
+    font-size: 11px;
+    text-align: center;
+    padding: 5px 10px;
+}
+
diff --git a/src/main/webapp/css/show_performance_digest.css b/src/main/webapp/css/show_performance_digest.css
new file mode 100644
index 0000000..5d0c0e6
--- /dev/null
+++ b/src/main/webapp/css/show_performance_digest.css
@@ -0,0 +1,82 @@
+/* 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.
+*/
+
+#header-container {
+    padding: 25px;
+}
+
+#load {
+    margin-top: 15px;
+}
+
+#device-select-wrapper {
+    margin-top: 9px;
+}
+
+#date {
+    margin-bottom: 0;
+    margin-top: 10px;
+}
+
+div.col.card.summary {
+    padding: 0 20px 20px;
+}
+
+.profiling-name {
+    font-weight: 200;
+    font-style: italic;
+    font-size: 1.4rem
+}
+
+.profiling-subtitle {
+    font-style: italic;
+    font-size: 12px;
+    margin-left: 2px;
+}
+
+.summary table {
+    width: 100%;
+    border-collapse: collapse;
+    border: 1px solid black;
+    font-size: 12px;
+    font-family: Roboto !important;
+}
+
+.summary table td, th {
+    padding: 2px;
+}
+
+.section-label {
+    border: 1px solid black;
+}
+
+.axis-label {
+    border-top: 1px solid lightgray;
+    border-right: 1px solid black;
+    text-align: right;
+}
+
+.cell {
+    border-top: 1px solid lightgray;
+    text-align: right;
+}
+
+.inner-cell {
+    border-right: 1px solid lightgray;
+}
+
+.outer-cell {
+    border-right: 1px solid black;
+}
diff --git a/src/main/webapp/css/show_plan_release.css b/src/main/webapp/css/show_plan_release.css
new file mode 100644
index 0000000..f55b8da
--- /dev/null
+++ b/src/main/webapp/css/show_plan_release.css
@@ -0,0 +1,23 @@
+/* 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.
+*/
+.page-button-wrapper {
+    top: 50%;
+    bottom: auto;
+    padding: 0;
+}
+#newer-wrapper {
+    left: 23px;
+    right: auto;
+}
diff --git a/src/main/webapp/css/show_release.css b/src/main/webapp/css/show_release.css
new file mode 100644
index 0000000..1f1758b
--- /dev/null
+++ b/src/main/webapp/css/show_release.css
@@ -0,0 +1,40 @@
+/* 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.
+*/
+
+.row .col.card.option {
+    padding: 10px 15px 10px 25px;
+    margin: 5px 0;
+    border-radius: 25px;
+}
+.entry {
+    font-size: 20px;
+    font-weight: 300;
+    position: relative;
+}
+#show-button-arrow {
+    margin-left: 3px;
+}
+#section-header {
+    cursor: default;
+    user-select: none;
+    color: #ee6e73;
+}
+#section-header:after {
+    border: 1px solid #ee6e73;
+    margin-top: 10px;
+    margin-bottom: 0;
+    display: block;
+    content: " ";
+}
diff --git a/src/main/webapp/css/show_table.css b/src/main/webapp/css/show_table.css
new file mode 100644
index 0000000..76d4379
--- /dev/null
+++ b/src/main/webapp/css/show_table.css
@@ -0,0 +1,156 @@
+/* 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.
+*/
+
+table {
+    font-family: Roboto !important;
+    font-size: 13px !important;
+    white-space: nowrap !important;
+}
+.table-header-cell {
+    background-color: white;
+    transition: max-width 1s;
+    max-width: 150px;
+}
+.table-header-cell:hover {
+    transition-delay: 0.5s;
+    max-width: 1000px;
+}
+.table-header-content.table-header-legend {
+    max-width: none;
+}
+.table-header-content {
+    font-weight: initial;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: inline-block;
+    max-width: inherit;
+    width: 100%;
+}
+.page-button-wrapper {
+    top: 50%;
+    bottom: auto;
+    padding: 0;
+}
+#newer-wrapper {
+    left: 23px;
+    right: auto;
+}
+#chart-holder {
+    padding: 30px 5px;
+}
+#pie-chart-div {
+    width: 100%;
+    height: 300px;
+}
+#profiling-container {
+    padding: 0;
+}
+#error-div {
+    padding: 30px 25px;
+}
+#profiling-body {
+    border: none;
+}
+#profiling-list {
+    max-height: 200px;
+    overflow-y: scroll;
+}
+.collapsible-header {
+    user-select: none;
+}
+.collapsible-link {
+    color: inherit;
+}
+.collection a.collection-item.profiling-point-name {
+    color: #616161;
+    font-size: 13px;
+    padding: 2px 0 2px 25px;
+}
+#legend-wrapper {
+    display: inline-block;
+    padding: 10px 15px 12px;
+}
+.btn.inline-btn {
+    border-radius: 50px;
+    height: auto;
+    line-height: inherit;
+    padding: 1px;
+    margin-left: 2px;
+}
+i.material-icons.inline-icon {
+    font-size: inherit;
+}
+a.legend-circle {
+    width: 15px;
+    height: 15px;
+    padding: 0;
+    border-radius: 15px;
+}
+.legend-header-cell {
+    text-transform: capitalize;
+}
+.legend-entry {
+    display: inline-block;
+    margin: 0 5px;
+    min-width: 50px;
+}
+.legend-bubble {
+    border-radius: 20px;
+    height: 20px;
+    width: 20px;
+    margin: 0 auto;
+}
+#pie-chart-wrapper {
+    padding: 25px 0;
+}
+.pie-chart-title {
+    cursor: default;
+}
+div.status-icon {
+    width: 10px;
+    height: 10px;
+    border-radius: 10px;
+    display: inline-block;
+    margin-left: 5px;
+}
+.test-case-status {
+    border-radius: 50px;
+    display: inline-block;
+    height: 100%;
+    width: 100%;
+}
+.test-case-status.width-1 {
+    width: calc(100% - 18px);
+}
+.TEST_CASE_RESULT_PASS {
+    background-color: #7FFF00;
+}
+.TEST_CASE_RESULT_FAIL {
+    background-color: #ff4d4d;
+}
+.TEST_CASE_RESULT_SKIP {
+    background-color: #A8A8A8;
+}
+.TEST_CASE_RESULT_EXCEPTION {
+    background-color: black;
+}
+.TEST_CASE_RESULT_TIMEOUT {
+    background-color: #9900CC;
+}
+.UNKNOWN_RESULT {
+    background-color: white;
+    border: 1px #A8A8A8 solid;
+}
diff --git a/src/main/webapp/css/show_test_runs_common.css b/src/main/webapp/css/show_test_runs_common.css
new file mode 100644
index 0000000..63d5301
--- /dev/null
+++ b/src/main/webapp/css/show_test_runs_common.css
@@ -0,0 +1,136 @@
+/* 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.
+*/
+.page-button-wrapper {
+    top: 50%;
+    bottom: auto;
+    padding: 0;
+}
+#newer-wrapper {
+    left: 23px;
+    right: auto;
+}
+#chart-holder {
+    padding: 30px 5px;
+}
+#pie-chart-div {
+    width: 100%;
+    height: 300px;
+}
+.row .col div.pie-chart-div {
+    width: 100%;
+    height: 255px;
+}
+#profiling-container {
+    padding: 0;
+}
+#error-div {
+    padding: 30px 25px;
+}
+#profiling-body {
+    border: none;
+}
+#profiling-list {
+    max-height: 200px;
+    overflow-y: scroll;
+}
+.collapsible-header {
+    user-select: none;
+}
+.collapsible-link {
+    color: inherit;
+}
+.collection a.collection-item.profiling-point-name {
+    color: #616161;
+    font-size: 13px;
+    padding: 2px 0 2px 25px;
+}
+#legend-wrapper {
+    display: inline-block;
+    padding: 10px 15px 12px;
+}
+.btn.inline-btn {
+    border-radius: 50px;
+    height: auto;
+    line-height: inherit;
+    padding: 1px;
+    margin-left: 2px;
+}
+i.material-icons.inline-icon {
+    font-size: inherit;
+}
+a.legend-circle {
+    width: 15px;
+    height: 15px;
+    padding: 0;
+    border-radius: 15px;
+}
+.legend-header-cell {
+    text-transform: capitalize;
+}
+.legend-entry {
+    display: inline-block;
+    margin: 0 5px;
+    min-width: 50px;
+}
+.legend-bubble {
+    border-radius: 20px;
+    height: 20px;
+    width: 20px;
+    margin: 0 auto;
+}
+#pie-chart-wrapper, .row .col.pie-chart-wrapper {
+    padding: 25px 0;
+}
+.pie-chart-title {
+    cursor: default;
+}
+div.status-icon {
+    width: 10px;
+    height: 10px;
+    border-radius: 10px;
+    display: inline-block;
+    margin-left: 5px;
+}
+.test-case-status {
+    border-radius: 50px;
+    display: inline-block;
+    height: 100%;
+    width: 100%;
+}
+.test-case-status.width-1 {
+    width: calc(100% - 18px);
+}
+.TEST_CASE_RESULT_PASS {
+    background-color: #4CAF50;
+}
+.TEST_CASE_RESULT_FAIL {
+    background-color: #F44336;
+}
+.TEST_CASE_RESULT_SKIP {
+    background-color: #A8A8A8;
+}
+.TEST_CASE_RESULT_EXCEPTION {
+    background-color: black;
+}
+.TEST_CASE_RESULT_TIMEOUT {
+    background-color: #9900CC;
+}
+.UNKNOWN_RESULT {
+    background-color: white;
+    border: 1px #A8A8A8 solid;
+}
+.tabs > div.indicator {
+    height: 3px;
+}
diff --git a/src/main/webapp/css/test_results.css b/src/main/webapp/css/test_results.css
new file mode 100644
index 0000000..f717e79
--- /dev/null
+++ b/src/main/webapp/css/test_results.css
@@ -0,0 +1,78 @@
+/* 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.
+*/
+li.test-run-container.active {
+    border-radius: 0 0 10px 10px;
+}
+.collapsible-header {
+    user-select: none;
+}
+.collapsible-header.disabled {
+    pointer-events: none;
+}
+.collapsible-header.test-run {
+    position: relative;
+}
+.test-run-metadata {
+    font-size: 13px;
+    line-height: 15px;
+    padding-top: 10px;
+    padding-bottom: 10px;
+    position: relative;
+    display: inline-block;
+    cursor: text;
+    user-select: initial;
+}
+.test-results.row {
+    margin: 0;
+    border-radius: 0 0 10px 10px;
+}
+.test-case-container {
+    border: 1px solid lightgray;
+    background: white;
+    padding: 10px;
+    margin-bottom: 25px;
+    max-height: 80%;
+    overflow: auto;
+}
+.indicator {
+    color: white;
+    font-size: 12px;
+    line-height: 20px;
+    font-weight: bold;
+    padding: 1px 6px;
+    margin-top: 10px;
+    min-width: 40px;
+    border-radius: 10px;
+}
+.indicator.padded {
+    margin-right: 5px;
+}
+.material-icons.expand-arrow {
+    right: 3px;
+    bottom: 0px;
+    position: absolute;
+    transition: transform 0.2s;
+}
+.rotate {
+    transform: rotate(-180deg);
+}
+i.material-icons.inline-icon {
+    font-size: inherit;
+}
+.test-run-label {
+    font-size: 18px;
+    line-height: 35px;
+    font-weight: 300;
+}
diff --git a/src/main/webapp/js/plan_runs.js b/src/main/webapp/js/plan_runs.js
new file mode 100644
index 0000000..6898389
--- /dev/null
+++ b/src/main/webapp/js/plan_runs.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.
+ */
+
+(function ($, moment) {
+
+  /**
+   * Display test plan metadata in a vertical popout.
+   * @param container The jquery object in which to insert the plan metadata.
+   * @param metadataList The list of metadata objects to render on the display.
+   */
+  function renderCard(container, entry) {
+    var card = $('<div class="col s12 m6 l4"></div>');
+    card.appendTo(container);
+    var div = $('<div class="hoverable card release-entry"></div>');
+    var startTime = entry.testPlanRun.startTimestamp;
+    var endTime = entry.testPlanRun.endTimestamp;
+    div.appendTo(card);
+    var span = $('<span></span>');
+    span.addClass('plan-run-metadata');
+    span.appendTo(div);
+    $('<b></b>').text(entry.deviceInfo).appendTo(span);
+    span.append('<br>');
+    $('<b></b>').text('VTS Build: ').appendTo(span);
+    span.append(entry.testPlanRun.testBuildId).append('<br>');
+    var timeString = (
+      moment().renderTime(startTime, false) + ' - ' +
+      moment().renderTime(endTime, true) + ' (' +
+      moment().renderDuration(endTime - startTime) + ')');
+    span.append(timeString);
+    var counter = $('<span></span>');
+    var color = entry.testPlanRun.failCount > 0 ? 'red' : 'green';
+    counter.addClass('counter center ' + color);
+    counter.append(
+      entry.testPlanRun.passCount + '/' +
+      (entry.testPlanRun.passCount + entry.testPlanRun.failCount));
+    counter.appendTo(div);
+    div.click(function () {
+      window.location.href = (
+        '/show_plan_run?plan=' + entry.testPlanRun.testPlanName +
+        '&time=' + entry.testPlanRun.startTimestamp);
+    })
+  }
+
+  $.fn.showPlanRuns = function(data) {
+    var self = $(this);
+    data.forEach(function (entry) {
+      renderCard(self, entry);
+    })
+  }
+
+})(jQuery, moment);
diff --git a/src/main/webapp/js/search_header.js b/src/main/webapp/js/search_header.js
new file mode 100644
index 0000000..0244815
--- /dev/null
+++ b/src/main/webapp/js/search_header.js
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.
+ */
+
+(function ($) {
+
+  function _validate(input, valueSet) {
+    var value = input.val();
+    if (valueSet.has(value) || !value) {
+      input.removeClass('invalid');
+    } else {
+      input.addClass('invalid');
+    }
+  }
+
+  function _createInput(key, config) {
+    var value = config.value;
+    var values = config.options.corpus;
+    var displayName = config.displayName;
+    var width = config.options.width || 's4';
+    var div = $('<div class="input-field col"></div>');
+    div.addClass(width);
+    var input = $('<input class="filter-input"></input>');
+    input.attr('type', config.options.type || 'text');
+    input.appendTo(div);
+    var label = $('<label></label>').text(displayName).appendTo(div);
+    if (value) {
+      input.attr('value', value);
+      label.addClass('active');
+    }
+    input.focusout(function() {
+      config.value = input.val();
+    });
+    if (values && values.length > 0) {
+      var valueSet = new Set(values);
+      input.sizedAutocomplete({
+        source: values,
+        classes: {
+          'ui-autocomplete': 'card search-bar-menu'
+        }
+      });
+      input.focusout(function() {
+        _validate(input, valueSet);
+      });
+    }
+    if (values && values.length > 0 && value) {
+      _validate(input, valueSet);
+    }
+    return div;
+  }
+
+  function _verifyCheckboxes(checkboxes, refreshObject) {
+    var oneChecked = checkboxes.presubmit || checkboxes.postsubmit;
+    if (!oneChecked) {
+      refreshObject.addClass('disabled');
+    } else {
+      refreshObject.removeClass('disabled');
+    }
+  }
+
+  function _createRunTypeBoxes(checkboxes, refreshObject) {
+    var container = $('<div class="run-type-wrapper col s12"></div>');
+    var presubmit = $('<input type="checkbox" id="presubmit"></input>');
+    presubmit.appendTo(container);
+    if (checkboxes.presubmit) {
+      presubmit.prop('checked', true);
+    }
+    container.append('<label for="presubmit">Presubmit</label>');
+    var postsubmit = $('<input type="checkbox" id="postsubmit"></input>');
+    postsubmit.appendTo(container);
+    if (checkboxes.postsubmit) {
+      postsubmit.prop('checked', true);
+    }
+    container.append('<label for="postsubmit">Postsubmit</label>');
+    presubmit.change(function() {
+      checkboxes.presubmit = presubmit.prop('checked');
+      _verifyCheckboxes(checkboxes, refreshObject);
+    });
+    postsubmit.change(function() {
+      checkboxes.postsubmit = postsubmit.prop('checked');
+      _verifyCheckboxes(checkboxes, refreshObject);
+    });
+    return container;
+  }
+
+  function _expand(
+      container, filters, checkboxes, onRefreshCallback, animate=true) {
+    var wrapper = $('<div class="search-wrapper"></div>');
+    var col = $('<div class="col s9"></div>');
+    col.appendTo(wrapper);
+    Object.keys(filters).forEach(function(key) {
+      col.append(_createInput(key, filters[key]));
+    });
+    var refreshCol = $('<div class="col s3 refresh-wrapper"></div>');
+    var refresh = $('<a class="btn-floating btn-medium red right waves-effect waves-light"></a>')
+      .append($('<i class="medium material-icons">cached</i>'))
+      .appendTo(refreshCol);
+    refresh.click(onRefreshCallback);
+    refreshCol.appendTo(wrapper);
+    if (Object.keys(checkboxes).length > 0) {
+      col.append(_createRunTypeBoxes(checkboxes, refresh));
+    }
+    if (animate) {
+      wrapper.hide().appendTo(container).slideDown({
+        duration: 350,
+        easing: "easeOutQuart",
+        queue: false
+      });
+    } else {
+      wrapper.appendTo(container);
+    }
+  }
+
+  function _renderHeader(
+      container, label, value, filters, checkboxes, expand, onRefreshCallback) {
+    var div = $('<div class="row card search-bar"></div>');
+    var wrapper = $('<div class="header-wrapper"></div>');
+    var header = $('<h5 class="section-header"></h5>');
+    $('<b></b>').text(label).appendTo(header);
+    $('<span></span>').text(value).appendTo(header);
+    header.appendTo(wrapper);
+    var iconWrapper = $('<div class="search-icon-wrapper"></div>');
+    $('<i class="material-icons">search</i>').appendTo(iconWrapper);
+    iconWrapper.appendTo(wrapper);
+    wrapper.appendTo(div);
+    if (expand) {
+      _expand(div, filters, checkboxes, onRefreshCallback, false);
+    } else {
+      var expanded = false;
+      iconWrapper.click(function() {
+        if (expanded) return;
+        expanded = true;
+        _expand(div, filters, checkboxes, onRefreshCallback);
+      });
+    }
+    div.appendTo(container);
+  }
+
+  function _addFilter(filters, displayName, keyName, options, defaultValue) {
+    filters[keyName] = {};
+    filters[keyName].displayName = displayName;
+    filters[keyName].value = defaultValue;
+    filters[keyName].options = options;
+  }
+
+  function _getOptionString(filters, checkboxes) {
+    var args = Object.keys(filters).reduce(function(acc, key) {
+      if (filters[key].value) {
+        return acc + '&' + key + '=' + encodeURIComponent(filters[key].value);
+      }
+      return acc;
+    }, '');
+    if (checkboxes.presubmit != undefined && checkboxes.presubmit) {
+      args += '&showPresubmit='
+    }
+    if (checkboxes.postsubmit != undefined && checkboxes.postsubmit) {
+      args += '&showPostsubmit='
+    }
+    return args;
+  }
+
+  /**
+   * Create a search header element.
+   * @param label The header label.
+   * @param value The value to display next to the label.
+   * @param onRefreshCallback The function to call on refresh.
+   */
+  $.fn.createSearchHeader = function(label, value, onRefreshCallback) {
+    var self = $(this);
+    $.widget('custom.sizedAutocomplete', $.ui.autocomplete, {
+      _resizeMenu : function() {
+        this.menu.element.outerWidth($('.search-bar .filter-input').width());
+      }
+    });
+    var filters = {};
+    var checkboxes = {};
+    var expandOnRender = false;
+    var displayed = false;
+    return {
+      /**
+       * Add a filter to the display.
+       * @param displayName The input placeholder/label text.
+       * @param keyName The URL key to use for the filter options.
+       * @param options A dict of additional options (e.g. width, type).
+       * @param defaultValue A default filter value.
+       */
+      addFilter : function(displayName, keyName, options, defaultValue) {
+        if (displayed) return;
+        _addFilter(filters, displayName, keyName, options, defaultValue);
+        if (defaultValue) expandOnRender = true;
+      },
+      /**
+       * Enable run type checkboxes in the filter options.
+       *
+       * This will display two checkboxes for selecting pre-/postsubmit runs.
+       * @param showPresubmit True if presubmit runs are selected.
+       * @param showPostsubmit True if postsubmit runs are selected.
+       *
+       */
+      addRunTypeCheckboxes: function(showPresubmit, showPostsubmit) {
+        if (displayed) return;
+        checkboxes['presubmit'] = showPresubmit;
+        checkboxes['postsubmit'] = showPostsubmit;
+        if (!showPostsubmit || showPresubmit) {
+          expandOnRender = true;
+        }
+      },
+      /**
+       * Display the created search bar.
+       *
+       * This must be called after filters have been added. After displaying, no
+       * modifications to the filter options will take effect.
+       */
+      display : function() {
+        displayed = true;
+        _renderHeader(
+          self, label, value, filters, checkboxes, expandOnRender,
+          onRefreshCallback);
+      },
+      /**
+       * Get the URL arguments string for the current set of filters.
+       * @returns a URI-encoded component with the search bar keys and values.
+       */
+      args : function () {
+        return _getOptionString(filters, checkboxes);
+      }
+    }
+  }
+
+})(jQuery);
diff --git a/src/main/webapp/js/test_results.js b/src/main/webapp/js/test_results.js
new file mode 100644
index 0000000..8f923fb
--- /dev/null
+++ b/src/main/webapp/js/test_results.js
@@ -0,0 +1,272 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.
+ */
+
+(function ($, moment) {
+
+  /**
+   * Display the log links in a modal window.
+   * @param logList A list of [name, url] tuples representing log links.
+   */
+  function showLogs(container, logList) {
+    if (!logList || logList.length == 0) return;
+
+    var logCollection = $('<ul class="collection"></ul>');
+    var entries = logList.reduce(function(acc, entry) {
+      if (!entry || entry.length == 0) return acc;
+      var link = '<a href="' + entry[1] + '"';
+      link += 'class="collection-item">' + entry[0] + '</li>';
+      return acc + link;
+    }, '');
+    logCollection.html(entries);
+
+    if (container.find('#info-modal').length == 0) {
+      var modal = $('<div id="info-modal" class="modal"></div>');
+      var content = $('<div class="modal-content"></div>');
+      content.append('<h4>Logs</h4>');
+      content.append('<div class="info-container"></div>');
+      content.appendTo(modal);
+      modal.appendTo(container);
+    }
+    var infoContainer = $('#info-modal>.modal-content>.info-container');
+    infoContainer.empty();
+    logCollection.appendTo(infoContainer);
+    $('#info-modal').openModal();
+  }
+
+  /**
+   * Get the nickname for a test case result.
+   *
+   * Removes the result prefix and suffix, extracting only the result name.
+   *
+   * @param testCaseResult The string name of a VtsReportMessage.TestCaseResult.
+   * @returns the string nickname of the result.
+   */
+  function getNickname(testCaseResult) {
+    return testCaseResult
+      .replace('TEST_CASE_RESULT_', '')
+      .replace('_RESULT', '')
+      .trim().toLowerCase();
+  }
+
+  /**
+   * Display test data in the body beneath a test run's metadata.
+   * @param container The jquery object in which to insert the test metadata.
+   * @param data The json object containing the columns to display.
+   * @param lineHeight The height of each list element.
+   */
+  function displayTestDetails(container, data, lineHeight) {
+    var nCol = data.length;
+    var width = 12 / nCol;
+    test = container;
+    var maxLines = 0;
+    data.forEach(function (column) {
+      if (column.data == undefined || column.name == undefined) {
+        return;
+      }
+      var colContainer =
+        $('<div class="col s' + width + ' test-col"></div>');
+      var col = $('<div class="test-case-container"></div>');
+      colContainer.appendTo(container);
+      var count = column.data.length;
+      $('<h5>' + getNickname(column.name) + ' (' + count + ')' + '</h5>')
+        .appendTo(colContainer).css('text-transform', 'capitalize');
+      col.appendTo(colContainer);
+      var list = $('<ul></ul>').appendTo(col);
+      column.data.forEach(function (testCase) {
+        $('<li></li>')
+          .text(testCase)
+          .addClass('test-case')
+          .css('font-size', lineHeight - 2)
+          .css('line-height', lineHeight + 'px')
+          .appendTo(list);
+      });
+      if (count > maxLines) {
+        maxLines = count;
+      }
+    });
+    var containers = container.find('.test-case-container');
+    containers.height(maxLines * lineHeight);
+  }
+
+  /**
+   * Click handler for displaying test run details.
+   * @param e The click event.
+   */
+  function testRunClick(e) {
+    var header = $(this);
+    var icon = header.find('.material-icons.expand-arrow');
+    var container = header.parent().find('.test-results');
+    var test = header.attr('test');
+    var time = header.attr('time');
+    var url = '/api/test_run?test=' + test + '&timestamp=' + time;
+    if (header.parent().hasClass('active')) {
+      header.parent().removeClass('active');
+      header.removeClass('active');
+      icon.removeClass('rotate');
+      header.siblings('.collapsible-body').stop(true, false).slideUp({
+        duration: 100,
+        easing: "easeOutQuart",
+        queue: false,
+        complete: function() { header.css('height', ''); }
+      });
+    } else {
+      container.empty();
+      header.parent().addClass('active');
+      header.addClass('active');
+      header.addClass('disabled');
+      icon.addClass('rotate');
+      $.get(url).done(function(data) {
+        displayTestDetails(container, data, 16);
+        header.siblings('.collapsible-body').stop(true, false).slideDown({
+          duration: 100,
+          easing: "easeOutQuart",
+          queue: false,
+          complete: function() { header.css('height', ''); }
+        });
+      }).fail(function() {
+        icon.removeClass('rotate');
+      }).always(function() {
+        header.removeClass('disabled');
+      });
+    }
+  }
+
+  /**
+   * Append a clickable indicator link to the container.
+   * @param container The jquery object to append the indicator to.
+   * @param content The text to display in the indicator.
+   * @param classes Additional space-delimited classes to add to the indicator.
+   * @param click The click handler to assign to the indicator.
+   * @returns The jquery object for the indicator.
+   */
+  function createClickableIndicator(container, content, classes, click) {
+    var link = $('<a></a>');
+    link.addClass('indicator right center padded hoverable waves-effect');
+    link.addClass(classes)
+    link.append(content);
+    link.appendTo(container);
+    link.click(click);
+    return link;
+  }
+
+  function displayTestMetadata(container, metadataList, showTestNames=false) {
+    var popout = $('<ul></ul>');
+    popout.attr('data-collapsible', 'expandable');
+    popout.addClass('collapsible popout test-runs');
+    popout.appendTo(container);
+    popout.unbind();
+    metadataList.forEach(function (metadata) {
+      var li = $('<li class="test-run-container"></li>');
+      li.appendTo(popout);
+      var div = $('<div></div>');
+      var test = metadata.testRun.testName;
+      var startTime = metadata.testRun.startTimestamp;
+      var endTime = metadata.testRun.endTimestamp;
+      div.attr('test', test);
+      div.attr('time', startTime);
+      div.addClass('collapsible-header test-run');
+      div.appendTo(li);
+      div.unbind().click(testRunClick);
+      var span = $('<span></span>');
+      span.addClass('test-run-metadata');
+      span.appendTo(div);
+      span.click(function() { return false; });
+      if (showTestNames) {
+          $('<span class="test-run-label"></span>').text(test).appendTo(span);
+          span.append('<br>');
+      }
+      $('<b></b>').text(metadata.deviceInfo).appendTo(span);
+      span.append('<br>');
+      $('<b></b>').text('ABI: ')
+            .appendTo(span)
+      span.append(metadata.abiInfo).append('<br>');
+      $('<b></b>').text('VTS Build: ')
+            .appendTo(span)
+      span.append(metadata.testRun.testBuildId).append('<br>');
+      $('<b></b>').text('Host: ')
+            .appendTo(span)
+      span.append(metadata.testRun.hostName).append('<br>');
+      var timeString = (
+        moment().renderTime(startTime, false) + ' - ' +
+        moment().renderTime(endTime, true) + ' (' +
+        moment().renderDuration(endTime - startTime) + ')');
+      span.append(timeString);
+      var indicator = $('<span></span>');
+      var color = metadata.testRun.failCount > 0 ? 'red' : 'green';
+      indicator.addClass('indicator right center ' + color);
+      indicator.append(
+        metadata.testRun.passCount + '/' +
+        (metadata.testRun.passCount + metadata.testRun.failCount));
+      indicator.appendTo(div);
+      if (metadata.testRun.coveredLineCount != undefined &&
+        metadata.testRun.totalLineCount != undefined) {
+        var url = (
+          '/show_coverage?testName=' + test + '&startTime=' + startTime);
+        covered = metadata.testRun.coveredLineCount;
+        total = metadata.testRun.totalLineCount;
+        covPct = Math.round(covered / total * 1000) / 10;
+        var color = 'red';
+        if (covPct > 20 && covPct < 70) {
+          color = 'orange';
+        } else if (covPct >= 70) {
+          color = 'green';
+        }
+        var coverage = (
+          'Coverage: ' + covered + '/' + total + ' (' + covPct + '%)');
+        createClickableIndicator(
+          div, coverage, color,
+          function () { window.location.href = url; return false; });
+      }
+      if (metadata.testRun.logLinks != undefined) {
+        createClickableIndicator(
+          div, 'Logs', 'grey lighten-1',
+          function () {
+            showLogs(popout, metadata.testRun.logLinks);
+            return false;
+          });
+      }
+      var expand = $('<i></i>');
+      expand.addClass('material-icons expand-arrow')
+      expand.text('expand_more');
+      expand.appendTo(div);
+      var body = $('<div></div>')
+        .addClass('collapsible-body test-results row grey lighten-4')
+        .appendTo(li);
+      if (metadata.testDetails != undefined) {
+        expand.addClass('rotate');
+        li.addClass('active');
+        div.addClass('active');
+        displayTestDetails(body, metadata.testDetails, 16);
+        div.siblings('.collapsible-body').stop(true, false).slideDown({
+          duration: 0,
+          queue: false,
+          complete: function() { div.css('height', ''); }
+        });
+      }
+    });
+  }
+
+  /**
+   * Display test metadata in a vertical popout.
+   * @param container The jquery object in which to insert the test metadata.
+   * @param metadataList The list of metadata objects to render on the display.
+   * @param showTestNames True to label each entry with the test module name.
+   */
+  $.fn.showTests = function(metadataList, showTestNames=false) {
+    displayTestMetadata($(this), metadataList, showTestNames);
+  }
+
+})(jQuery, moment);
diff --git a/src/main/webapp/js/time.js b/src/main/webapp/js/time.js
new file mode 100644
index 0000000..c5fbef6
--- /dev/null
+++ b/src/main/webapp/js/time.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2017 Google Inc. All Rights Reserved.
+ *
+ * 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.
+ */
+
+(function (moment) {
+
+  /**
+   * Renders a timestamp in the user timezone.
+   * @param timestamp The long timestamp to render (in microseconds).
+   * @param showTimezone True if the timezone should be rendered, false otherwise.
+   * @returns the string-formatted version of the provided timestamp.
+   */
+  moment.prototype.renderTime = function (timestamp, showTimezone) {
+    var time = moment(timestamp / 1000);
+    var format = 'H:mm:ss';
+    if (!time.isSame(moment(), 'd')) {
+        format = 'M/D/YY ' + format;
+    }
+    if (!!showTimezone) {
+        format = format + 'ZZ';
+    }
+    return time.format(format);
+  }
+
+  /**
+   * Renders a duration in the user timezone.
+   * @param durationTimestamp The long duration to render (in microseconds).
+   * @returns the string-formatted duration of the provided duration timestamp.
+   */
+  moment.prototype.renderDuration = function (durationTimestamp) {
+    var fmt = 's[s]';
+    var duration = moment.utc(durationTimestamp / 1000);
+    if (duration.hours() > 0) {
+      fmt = 'H[h], m[m], ' + fmt;
+    } else if (duration.minutes() > 0) {
+      fmt = 'm[m], ' + fmt;
+    }
+    return duration.format(fmt);
+  }
+
+})(moment);
diff --git a/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java b/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java
new file mode 100644
index 0000000..c8adb69
--- /dev/null
+++ b/src/test/java/com/android/vts/servlet/VtsPerformanceJobServletTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.servlet;
+
+import static org.junit.Assert.*;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.android.vts.util.PerformanceSummary;
+import com.android.vts.util.ProfilingPointSummary;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class VtsPerformanceJobServletTest {
+    private final LocalServiceTestHelper helper =
+            new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+
+    private static final String LABEL = "testLabel";
+    private static final String ROOT = "src/test/res/servlet";
+    private static final String[] LABELS = new String[] {"label1", "label2", "label3"};
+    private static final long[] HIGH_VALS = new long[] {10, 20, 30};
+    private static final long[] LOW_VALS = new long[] {1, 2, 3};
+
+    List<PerformanceSummary> dailySummaries;
+    List<String> legendLabels;
+
+    /**
+     * Helper method for creating ProfilingPointRunEntity objects.
+     *
+     * @param labels The list of data labels.
+     * @param values The list of data values. Must be equal in size to the labels list.
+     * @param regressionMode The regression mode.
+     * @return A ProfilingPointRunEntity with specified arguments.
+     */
+    private static ProfilingPointRunEntity createProfilingReport(
+            String[] labels, long[] values, VtsProfilingRegressionMode regressionMode) {
+        List<String> labelList = Arrays.asList(labels);
+        List<Long> valueList = new ArrayList<>();
+        for (long value : values) {
+            valueList.add(value);
+        }
+        return new ProfilingPointRunEntity(KeyFactory.createKey(TestEntity.KIND, "test"), "name", 0,
+                regressionMode.getNumber(), labelList, valueList, "", "", null);
+    }
+
+    /** Asserts whether text is the same as the contents in the baseline file specified. */
+    private static void compareToBaseline(String text, String baselineFilename)
+            throws FileNotFoundException, IOException {
+        File f = new File(ROOT, baselineFilename);
+        String baseline = "";
+        try (BufferedReader br = new BufferedReader(new FileReader(f))) {
+            StringBuilder sb = new StringBuilder();
+            String line = br.readLine();
+
+            while (line != null) {
+                sb.append(line);
+                line = br.readLine();
+            }
+            baseline = sb.toString();
+        }
+        assertEquals(baseline, text);
+    }
+
+    @Before
+    public void setUp() {
+        helper.setUp();
+    }
+
+    @After
+    public void tearDown() {
+        helper.tearDown();
+    }
+
+    public void setUp(boolean grouped) {
+        dailySummaries = new ArrayList<>();
+        legendLabels = new ArrayList<>();
+        legendLabels.add("");
+
+        // Add today's data
+        PerformanceSummary today = new PerformanceSummary();
+        ProfilingPointSummary summary = new ProfilingPointSummary();
+        VtsProfilingRegressionMode mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        ProfilingPointRunEntity pt = createProfilingReport(LABELS, HIGH_VALS, mode);
+        if (grouped) {
+            summary.updateLabel(pt, LABEL);
+            summary.updateLabel(pt, LABEL);
+        } else {
+            summary.update(pt);
+            summary.update(pt);
+        }
+        today.insertProfilingPointSummary("p1", summary);
+
+        summary = new ProfilingPointSummary();
+        mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_DECREASING;
+        pt = createProfilingReport(LABELS, LOW_VALS, mode);
+        if (grouped) {
+            summary.updateLabel(pt, LABEL);
+            summary.updateLabel(pt, LABEL);
+        } else {
+            summary.update(pt);
+            summary.update(pt);
+        }
+        today.insertProfilingPointSummary("p2", summary);
+        dailySummaries.add(today);
+        legendLabels.add("today");
+
+        // Add yesterday data with regressions
+        PerformanceSummary yesterday = new PerformanceSummary();
+        summary = new ProfilingPointSummary();
+        mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        pt = createProfilingReport(LABELS, LOW_VALS, mode);
+        if (grouped) {
+            summary.updateLabel(pt, LABEL);
+            summary.updateLabel(pt, LABEL);
+        } else {
+            summary.update(pt);
+            summary.update(pt);
+        }
+        yesterday.insertProfilingPointSummary("p1", summary);
+
+        summary = new ProfilingPointSummary();
+        mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_DECREASING;
+        pt = createProfilingReport(LABELS, HIGH_VALS, mode);
+        if (grouped) {
+            summary.updateLabel(pt, LABEL);
+            summary.updateLabel(pt, LABEL);
+        } else {
+            summary.update(pt);
+            summary.update(pt);
+        }
+        yesterday.insertProfilingPointSummary("p2", summary);
+        dailySummaries.add(yesterday);
+        legendLabels.add("yesterday");
+
+        // Add last week data without regressions
+        PerformanceSummary lastWeek = new PerformanceSummary();
+        summary = new ProfilingPointSummary();
+        mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        pt = createProfilingReport(LABELS, HIGH_VALS, mode);
+        summary.update(pt);
+        summary.update(pt);
+        lastWeek.insertProfilingPointSummary("p1", summary);
+
+        summary = new ProfilingPointSummary();
+        mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_DECREASING;
+        pt = createProfilingReport(LABELS, LOW_VALS, mode);
+        summary.update(pt);
+        summary.update(pt);
+        lastWeek.insertProfilingPointSummary("p2", summary);
+        dailySummaries.add(lastWeek);
+        legendLabels.add("last week");
+    }
+
+    /**
+     * End-to-end test of performance report in the normal case. The normal case is when a profiling
+     * point is added or removed from the test.
+     */
+    @Test
+    public void testPerformanceSummaryNormal() throws FileNotFoundException, IOException {
+        setUp(false);
+        String output =
+                VtsPerformanceJobServlet.getPeformanceSummary("test", dailySummaries, legendLabels);
+        compareToBaseline(output, "performanceSummary1.html");
+    }
+
+    /** End-to-end test of performance report when a profiling point was removed in the latest run.
+     */
+    @Test
+    public void testPerformanceSummaryDroppedProfilingPoint()
+            throws FileNotFoundException, IOException {
+        setUp(false);
+        PerformanceSummary yesterday = dailySummaries.get(dailySummaries.size() - 1);
+        ProfilingPointSummary summary = new ProfilingPointSummary();
+        VtsProfilingRegressionMode mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        ProfilingPointRunEntity pt = createProfilingReport(LABELS, HIGH_VALS, mode);
+        summary.update(pt);
+        summary.update(pt);
+        yesterday.insertProfilingPointSummary("p3", summary);
+        String output =
+                VtsPerformanceJobServlet.getPeformanceSummary("test", dailySummaries, legendLabels);
+        compareToBaseline(output, "performanceSummary2.html");
+    }
+
+    /** End-to-end test of performance report when a profiling point was added in the latest run. */
+    @Test
+    public void testPerformanceSummaryAddedProfilingPoint()
+            throws FileNotFoundException, IOException {
+        setUp(false);
+        PerformanceSummary today = dailySummaries.get(0);
+        ProfilingPointSummary summary = new ProfilingPointSummary();
+        VtsProfilingRegressionMode mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        ProfilingPointRunEntity pt = createProfilingReport(LABELS, HIGH_VALS, mode);
+        summary.update(pt);
+        summary.update(pt);
+        today.insertProfilingPointSummary("p3", summary);
+        String output =
+                VtsPerformanceJobServlet.getPeformanceSummary("test", dailySummaries, legendLabels);
+        compareToBaseline(output, "performanceSummary3.html");
+    }
+
+    /** End-to-end test of performance report labels are grouped (e.g. as if using unlabeled data)
+     */
+    @Test
+    public void testPerformanceSummaryGroupedNormal() throws FileNotFoundException, IOException {
+        setUp(true);
+        String output =
+                VtsPerformanceJobServlet.getPeformanceSummary("test", dailySummaries, legendLabels);
+        compareToBaseline(output, "performanceSummary4.html");
+    }
+}
diff --git a/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java b/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java
new file mode 100644
index 0000000..cc529be
--- /dev/null
+++ b/src/test/java/com/android/vts/util/ProfilingPointSummaryTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import static org.junit.Assert.*;
+
+import com.android.vts.entity.ProfilingPointRunEntity;
+import com.android.vts.entity.TestEntity;
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ProfilingPointSummaryTest {
+    private final LocalServiceTestHelper helper =
+            new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+    private static String[] labels = new String[] {"label1", "label2", "label3"};
+    private static long[] values = new long[] {1, 2, 3};
+    private static ProfilingPointSummary summary;
+
+    /**
+     * Helper method for creating ProfilingPointRunEntity objects.
+     *
+     * @param labels The list of data labels.
+     * @param values The list of data values. Must be equal in size to the labels list.
+     * @param regressionMode The regression mode.
+     * @return A ProfilingPointRunEntity with specified arguments.
+     */
+    private static ProfilingPointRunEntity createProfilingReport(
+            String[] labels, long[] values, VtsProfilingRegressionMode regressionMode) {
+        List<String> labelList = Arrays.asList(labels);
+        List<Long> valueList = new ArrayList<>();
+        for (long value : values) {
+            valueList.add(value);
+        }
+        return new ProfilingPointRunEntity(KeyFactory.createKey(TestEntity.KIND, "test"), "name", 0,
+                regressionMode.getNumber(), labelList, valueList, "x", "y", null);
+    }
+
+    @Before
+    public void setUp() {
+        helper.setUp();
+        summary = new ProfilingPointSummary();
+        VtsProfilingRegressionMode mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        ProfilingPointRunEntity pt = createProfilingReport(labels, values, mode);
+        summary.update(pt);
+    }
+
+    @After
+    public void tearDown() {
+        helper.tearDown();
+    }
+
+    /** Test that all labels are found by hasLabel. */
+    @Test
+    public void testHasLabel() {
+        for (String label : labels) {
+            assertTrue(summary.hasLabel(label));
+        }
+    }
+
+    /** Test that invalid labels are not found by hasLabel. */
+    @Test
+    public void testInvalidHasLabel() {
+        assertFalse(summary.hasLabel("bad label"));
+    }
+
+    /** Test that all stat summaries can be retrieved by profiling point label. */
+    @Test
+    public void testGetStatSummary() {
+        for (String label : labels) {
+            StatSummary stats = summary.getStatSummary(label);
+            assertNotNull(stats);
+            assertEquals(label, stats.getLabel());
+        }
+    }
+
+    /** Test that the getStatSummary method returns null when the label is not present. */
+    @Test
+    public void testInvalidGetStatSummary() {
+        StatSummary stats = summary.getStatSummary("bad label");
+        assertNull(stats);
+    }
+
+    /** Test that StatSummary objects are iterated in the order that the labels were provided. */
+    @Test
+    public void testIterator() {
+        VtsProfilingRegressionMode mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        ProfilingPointRunEntity pt = createProfilingReport(labels, values, mode);
+        summary.update(pt);
+
+        int i = 0;
+        for (StatSummary stats : summary) {
+            assertEquals(labels[i++], stats.getLabel());
+        }
+    }
+
+    /** Test that the updateLabel method updates the StatSummary for just the label provided. */
+    @Test
+    public void testUpdateLabelGrouped() {
+        summary = new ProfilingPointSummary();
+        VtsProfilingRegressionMode mode = VtsProfilingRegressionMode.VTS_REGRESSION_MODE_INCREASING;
+        ProfilingPointRunEntity pt = createProfilingReport(labels, values, mode);
+        summary.updateLabel(pt, labels[0]);
+
+        // Ensure the label specified is present and has been updated for each data point.
+        assertTrue(summary.hasLabel(labels[0]));
+        assertNotNull(summary.getStatSummary(labels[0]));
+        assertEquals(summary.getStatSummary(labels[0]).getCount(), labels.length);
+
+        // Check that the other labels were not updated.
+        for (int i = 1; i < labels.length; i++) {
+            assertFalse(summary.hasLabel(labels[i]));
+        }
+    }
+}
diff --git a/src/test/java/com/android/vts/util/StatSummaryTest.java b/src/test/java/com/android/vts/util/StatSummaryTest.java
new file mode 100644
index 0000000..333e3b5
--- /dev/null
+++ b/src/test/java/com/android/vts/util/StatSummaryTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.vts.util;
+
+import static org.junit.Assert.*;
+
+import com.android.vts.proto.VtsReportMessage.VtsProfilingRegressionMode;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+
+public class StatSummaryTest {
+    private static double threshold = 0.0000000001;
+    private StatSummary test;
+
+    @Before
+    public void setUp() {
+        test = new StatSummary("label", VtsProfilingRegressionMode.VTS_REGRESSION_MODE_DECREASING);
+    }
+
+    /** Test computation of average. */
+    @Test
+    public void testAverage() {
+        int n = 1000;
+        double mean = (n - 1) / 2.0;
+        for (int i = 0; i < n; i++) {
+            test.updateStats(i);
+        }
+        assertEquals(n, test.getCount(), threshold);
+        assertEquals(mean, test.getMean(), threshold);
+    }
+
+    /** Test computation of minimum. */
+    @Test
+    public void testMin() {
+        double min = Double.MAX_VALUE;
+        int n = 1000;
+        Random rand = new Random();
+        for (int i = 0; i < n; i++) {
+            double value = rand.nextInt(1000);
+            if (value < min)
+                min = value;
+            test.updateStats(value);
+        }
+        assertEquals(n, test.getCount(), threshold);
+        assertEquals(min, test.getMin(), threshold);
+    }
+
+    /** Test computation of maximum. */
+    @Test
+    public void testMax() {
+        double max = Double.MIN_VALUE;
+        int n = 1000;
+        Random rand = new Random();
+        for (int i = 0; i < n; i++) {
+            double value = rand.nextInt(1000);
+            if (value > max)
+                max = value;
+            test.updateStats(value);
+        }
+        assertEquals(max, test.getMax(), threshold);
+    }
+
+    /** Test computation of standard deviation. */
+    @Test
+    public void testStd() {
+        int n = 1000;
+        double[] values = new double[n];
+        Random rand = new Random();
+        double sum = 0.0;
+        for (int i = 0; i < n; i++) {
+            values[i] = rand.nextInt(1000);
+            sum += values[i];
+            test.updateStats(values[i]);
+        }
+        double mean = sum / n;
+        double sumSq = 0;
+        for (int i = 0; i < n; i++) {
+            sumSq += (values[i] - mean) * (values[i] - mean);
+        }
+        double std = Math.sqrt(sumSq / (n - 1));
+        assertEquals(std, test.getStd(), threshold);
+    }
+}
diff --git a/src/test/res/driver/chromedriver b/src/test/res/driver/chromedriver
new file mode 100755
index 0000000..bbb06c3
--- /dev/null
+++ b/src/test/res/driver/chromedriver
Binary files differ
diff --git a/src/test/res/driver/config.properties b/src/test/res/driver/config.properties
new file mode 100644
index 0000000..939fa90
--- /dev/null
+++ b/src/test/res/driver/config.properties
@@ -0,0 +1 @@
+LOCALHOST=localhost:8080
\ No newline at end of file
diff --git a/src/test/res/servlet/performanceSummary1.html b/src/test/res/servlet/performanceSummary1.html
new file mode 100644
index 0000000..3b01593
--- /dev/null
+++ b/src/test/res/servlet/performanceSummary1.html
@@ -0,0 +1 @@
+<p style='font-family: arial'><b>test</b></p><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p1</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Lower values are better. Minimum is the best-case performance.</i><br><br><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p2</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Higher values are better. Maximum is the best-case performance.</i><br><br>
\ No newline at end of file
diff --git a/src/test/res/servlet/performanceSummary2.html b/src/test/res/servlet/performanceSummary2.html
new file mode 100644
index 0000000..3b01593
--- /dev/null
+++ b/src/test/res/servlet/performanceSummary2.html
@@ -0,0 +1 @@
+<p style='font-family: arial'><b>test</b></p><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p1</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Lower values are better. Minimum is the best-case performance.</i><br><br><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p2</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Higher values are better. Maximum is the best-case performance.</i><br><br>
\ No newline at end of file
diff --git a/src/test/res/servlet/performanceSummary3.html b/src/test/res/servlet/performanceSummary3.html
new file mode 100644
index 0000000..1ff1028
--- /dev/null
+++ b/src/test/res/servlet/performanceSummary3.html
@@ -0,0 +1 @@
+<p style='font-family: arial'><b>test</b></p><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p1</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Lower values are better. Minimum is the best-case performance.</i><br><br><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p2</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td class='' style='background-color: rgba(255, 0, 0, -0.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>0 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Higher values are better. Maximum is the best-case performance.</i><br><br><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p3</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label1</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label2</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>label3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Lower values are better. Minimum is the best-case performance.</i><br><br>
\ No newline at end of file
diff --git a/src/test/res/servlet/performanceSummary4.html b/src/test/res/servlet/performanceSummary4.html
new file mode 100644
index 0000000..8fc8997
--- /dev/null
+++ b/src/test/res/servlet/performanceSummary4.html
@@ -0,0 +1 @@
+<p style='font-family: arial'><b>test</b></p><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p1</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Min (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Min</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>testLabel</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>10</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>8.94</td><td class='' style='background-color: rgba(255, 0, 0, 18.0); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>900 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>1<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0.89</td><td></td><td></td><td></td><td></td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Lower values are better. Minimum is the best-case performance.</i><br><br><table cellpadding='2' style='width: 100%; border-collapse: collapse; border: 1px solid black; font-size: 12px; font-family: arial;'><tr><td colspan='16'>p2</td></tr><tr><td colspan='16'></td></tr><tr><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='1'></th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='3'>today</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>yesterday</th><th style='border: 1px solid black; border-bottom: none; background-color: lightgray;' colspan='4'>last week</th></tr><tr><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'></th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>&Delta;Max (%)</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Max</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Mean</th><th style='border: 1px solid black; border-bottom-width: 2px; border-top: 1px dotted gray; background-color: lightgray;'>Std</th></tr><tr><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right; background-color: lightgray;'>testLabel</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>3</td><td style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>2</td><td style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>0.89</td><td class='' style='background-color: rgba(255, 0, 0, 1.8); border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>-90 %</td><td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>30<td class='' style='border-top: 1px dotted gray; border-right: 1px dotted gray; text-align: right;'>20<td class='' style='border-top: 1px dotted gray; border-right: 2px solid black; text-align: right;'>8.94</td><td></td><td></td><td></td><td></td></tr></table><i style='font-family: arial; font-size: 12px'>Note: Higher values are better. Maximum is the best-case performance.</i><br><br>
\ No newline at end of file