Merge changes from topic "hal-api-entity"
am: f2d725478c

Change-Id: I07afa1e645d65cbe4736ab0641eef97a3230dbdc
diff --git a/src/main/java/com/android/vts/api/BaseApiServlet.java b/src/main/java/com/android/vts/api/BaseApiServlet.java
index 1ad0237..b7384a7 100644
--- a/src/main/java/com/android/vts/api/BaseApiServlet.java
+++ b/src/main/java/com/android/vts/api/BaseApiServlet.java
@@ -17,61 +17,51 @@
 package com.android.vts.api;
 
 import com.google.apphosting.api.ApiProxy;
-import com.google.gson.Gson;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.Properties;
 import java.util.logging.Logger;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-/**
- * An abstract class to be subclassed to create API Servlet
- */
+/** An abstract class to be subclassed to create API Servlet */
 public class BaseApiServlet extends HttpServlet {
 
-  private static final Logger logger =
-      Logger.getLogger(BaseApiServlet.class.getName());
+    private static final Logger logger = Logger.getLogger(BaseApiServlet.class.getName());
 
-  /**
-   * System Configuration Property class
-   */
-  protected Properties systemConfigProp = new Properties();
+    /** System Configuration Property class */
+    protected Properties systemConfigProp = new Properties();
 
-  /**
-   * Appengine server host name
-   */
-  protected String hostName;
+    /** Appengine server host name */
+    protected String hostName;
 
-  @Override
-  public void init(ServletConfig cfg) throws ServletException {
-    super.init(cfg);
+    /**
+     * This variable is for maximum number of entities per transaction You can find the detail here
+     * (https://cloud.google.com/datastore/docs/concepts/limits)
+     */
+    protected int MAX_ENTITY_SIZE_PER_TRANSACTION = 300;
 
-    ApiProxy.Environment env = ApiProxy.getCurrentEnvironment();
-    hostName = env.getAttributes().get("com.google.appengine.runtime.default_version_hostname")
-        .toString();
-    try {
-      InputStream defaultInputStream =
-          BaseApiServlet.class
-              .getClassLoader()
-              .getResourceAsStream("config.properties");
-      systemConfigProp.load(defaultInputStream);
+    @Override
+    public void init(ServletConfig cfg) throws ServletException {
+        super.init(cfg);
 
-    } catch (FileNotFoundException e) {
-      e.printStackTrace();
-    } catch (IOException e) {
-      e.printStackTrace();
+        systemConfigProp =
+                Properties.class.cast(cfg.getServletContext().getAttribute("systemConfigProp"));
+
+        this.MAX_ENTITY_SIZE_PER_TRANSACTION =
+                Integer.parseInt(systemConfigProp.getProperty("datastore.maxEntitySize"));
+
+        ApiProxy.Environment env = ApiProxy.getCurrentEnvironment();
+        hostName =
+                env.getAttributes()
+                        .get("com.google.appengine.runtime.default_version_hostname")
+                        .toString();
     }
-  }
 
-  protected void setAccessControlHeaders(HttpServletResponse resp) {
-    resp.setHeader("Access-Control-Allow-Origin", hostName);
-    resp.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS, DELETE");
-    resp.addHeader("Access-Control-Allow-Headers", "Content-Type");
-    resp.addHeader("Access-Control-Max-Age", "86400");
-  }
+    protected void setAccessControlHeaders(HttpServletResponse resp) {
+        resp.setHeader("Access-Control-Allow-Origin", hostName);
+        resp.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS, DELETE");
+        resp.addHeader("Access-Control-Allow-Headers", "Content-Type");
+        resp.addHeader("Access-Control-Max-Age", "86400");
+    }
 }
diff --git a/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java b/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java
index ac69b84..d331d4b 100644
--- a/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java
+++ b/src/main/java/com/android/vts/api/BigtableLegacyJsonServlet.java
@@ -150,7 +150,6 @@
         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/CoverageRestServlet.java b/src/main/java/com/android/vts/api/CoverageRestServlet.java
index 217b740..165db8d 100644
--- a/src/main/java/com/android/vts/api/CoverageRestServlet.java
+++ b/src/main/java/com/android/vts/api/CoverageRestServlet.java
@@ -20,8 +20,10 @@
 import com.android.vts.entity.CoverageEntity;
 import com.android.vts.entity.TestCoverageStatusEntity;
 import com.android.vts.entity.TestPlanRunEntity;
+import com.android.vts.entity.TestRunEntity;
 import com.google.gson.Gson;
 import com.googlecode.objectify.Key;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -104,9 +106,11 @@
         List<List<String>> allCoveredHalApiList = new ArrayList();
 
         Key<TestPlanRunEntity> key = Key.create(urlSafeKey);
-        TestPlanRunEntity testPlanRunEntity = ofy().load().key(key).now();
+        System.out.println("urlSafekey => " + urlSafeKey);
+        TestPlanRunEntity testPlanRunEntity = ofy().load().key(key).safe();
 
-        for (Key testRunKey : testPlanRunEntity.getTestRuns()) {
+        System.out.println("testPlanRunEntity => " + testPlanRunEntity);
+        for (Key<TestRunEntity> testRunKey : testPlanRunEntity.getTestRuns()) {
             List<ApiCoverageEntity> apiCoverageEntityList =
                     ofy().load().type(ApiCoverageEntity.class).ancestor(testRunKey).list();
             for (ApiCoverageEntity apiCoverageEntity : apiCoverageEntityList) {
diff --git a/src/main/java/com/android/vts/api/DatastoreRestServlet.java b/src/main/java/com/android/vts/api/DatastoreRestServlet.java
index 42d7a60..5ddf18d 100644
--- a/src/main/java/com/android/vts/api/DatastoreRestServlet.java
+++ b/src/main/java/com/android/vts/api/DatastoreRestServlet.java
@@ -16,58 +16,60 @@
 
 package com.android.vts.api;
 
+import com.android.vts.entity.ApiCoverageEntity;
+import com.android.vts.entity.BranchEntity;
+import com.android.vts.entity.BuildTargetEntity;
+import com.android.vts.entity.CodeCoverageEntity;
+import com.android.vts.entity.CoverageEntity;
+import com.android.vts.entity.DashboardEntity;
+import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.HalApiEntity;
+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.proto.VtsReportMessage;
 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.servlet.BaseServlet;
-import com.android.vts.util.DatastoreHelper;
-import com.android.vts.util.EmailHelper;
 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.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.util.Properties;
-import java.util.logging.Level;
-import java.util.logging.Logger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+
+import com.google.appengine.api.datastore.Key;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.codec.binary.Base64;
 
+import static com.googlecode.objectify.ObjectifyService.ofy;
+
+@Slf4j
 /** REST endpoint for posting data to the Dashboard. */
-public class DatastoreRestServlet extends HttpServlet {
+public class DatastoreRestServlet extends BaseApiServlet {
     private static String SERVICE_CLIENT_ID;
     private static final String SERVICE_NAME = "VTS Dashboard";
-    private static final Logger logger = Logger.getLogger(DatastoreRestServlet.class.getName());
-
-    /** System Configuration Property class */
-    protected Properties systemConfigProp = new Properties();
 
     @Override
     public void init(ServletConfig cfg) throws ServletException {
         super.init(cfg);
 
-        try {
-            InputStream defaultInputStream =
-                    DatastoreRestServlet.class
-                            .getClassLoader()
-                            .getResourceAsStream("config.properties");
-            systemConfigProp.load(defaultInputStream);
-
-            SERVICE_CLIENT_ID = systemConfigProp.getProperty("appengine.serviceClientID");
-        } catch (FileNotFoundException e) {
-            e.printStackTrace();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
+        SERVICE_CLIENT_ID = this.systemConfigProp.getProperty("appengine.serviceClientID");
     }
 
     @Override
@@ -81,14 +83,15 @@
             postMessage = DashboardPostMessage.parseFrom(value);
         } catch (IOException e) {
             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-            logger.log(Level.WARNING, "Invalid proto: " + e.getLocalizedMessage());
+            log.error("Invalid proto: " + e.getLocalizedMessage());
             return;
         }
 
         // Verify service account access token.
         if (postMessage.hasAccessToken()) {
             String accessToken = postMessage.getAccessToken();
-            logger.log(Level.INFO, "accessToken => " + accessToken);
+            log.debug("accessToken => " + accessToken);
+
             GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
             Oauth2 oauth2 =
                     new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
@@ -97,23 +100,461 @@
             Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(accessToken).execute();
             if (tokenInfo.getIssuedTo().equals(SERVICE_CLIENT_ID)) {
                 for (TestReportMessage testReportMessage : postMessage.getTestReportList()) {
-                    DatastoreHelper.insertTestReport(testReportMessage);
+                    this.insertTestReport(testReportMessage);
                 }
 
                 for (TestPlanReportMessage planReportMessage :
                         postMessage.getTestPlanReportList()) {
-                    DatastoreHelper.insertTestPlanReport(planReportMessage);
+                    this.insertTestPlanReport(planReportMessage);
                 }
 
                 response.setStatus(HttpServletResponse.SC_OK);
             } else {
-                logger.log(Level.WARNING, "service_client_id didn't match!");
-                logger.log(Level.INFO, "SERVICE_CLIENT_ID => " + tokenInfo.getIssuedTo());
+                log.warn("service_client_id didn't match!");
+                log.debug("SERVICE_CLIENT_ID => " + tokenInfo.getIssuedTo());
                 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
             }
         } else {
-            logger.log(Level.WARNING, "postMessage do not contain any accessToken!");
+            log.error("postMessage do not contain any accessToken!");
             response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
         }
     }
+
+    /**
+     * Upload data from a test report message
+     *
+     * @param report The test report containing data to upload.
+     */
+    private void insertTestReport(TestReportMessage report) {
+
+        if (!report.hasStartTimestamp()
+                || !report.hasEndTimestamp()
+                || !report.hasTest()
+                || !report.hasHostInfo()
+                || !report.hasBuildInfo()) {
+            // missing information
+            log.error("Missing information in report !");
+            return;
+        }
+
+        List<TestEntity> testEntityList = new ArrayList<>();
+        List<TestRunEntity> testRunEntityList = new ArrayList<>();
+        List<BranchEntity> branchEntityList = new ArrayList<>();
+        List<BuildTargetEntity> buildTargetEntityList = new ArrayList<>();
+        List<CoverageEntity> coverageEntityList = new ArrayList<>();
+        List<CodeCoverageEntity> codeCoverageEntityList = new ArrayList<>();
+        List<DeviceInfoEntity> deviceInfoEntityList = new ArrayList<>();
+        List<ProfilingPointRunEntity> profilingPointRunEntityList = new ArrayList<>();
+        List<TestCaseRunEntity> testCaseRunEntityList = new ArrayList<>();
+        List<ApiCoverageEntity> apiCoverageEntityList = new ArrayList<>();
+
+        List<?> allEntityList =
+                Arrays.asList(
+                        testEntityList,
+                        branchEntityList,
+                        buildTargetEntityList,
+                        coverageEntityList,
+                        codeCoverageEntityList,
+                        deviceInfoEntityList,
+                        profilingPointRunEntityList,
+                        testCaseRunEntityList,
+                        apiCoverageEntityList,
+                        testRunEntityList);
+
+        long passCount = 0;
+        long failCount = 0;
+        long coveredLineCount = 0;
+        long totalLineCount = 0;
+
+        Set<Key> buildTargetKeys = new HashSet<>();
+        Set<Key> branchKeys = new HashSet<>();
+        List<Key> profilingPointKeyList = new ArrayList<>();
+        List<String> linkList = new ArrayList<>();
+
+        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();
+
+        TestEntity testEntity = new TestEntity(testName);
+
+        com.googlecode.objectify.Key testRunKey =
+                testEntity.getTestRunKey(report.getStartTimestamp());
+
+        testEntityList.add(testEntity);
+
+        int testCaseRunEntityIndex = 0;
+        testCaseRunEntityList.add(new TestCaseRunEntity());
+        // Process test cases
+        for (VtsReportMessage.TestCaseReportMessage testCase : report.getTestCaseList()) {
+            String testCaseName = testCase.getName().toStringUtf8();
+            VtsReportMessage.TestCaseResult result = testCase.getTestResult();
+            // Track global pass/fail counts
+            if (result == VtsReportMessage.TestCaseResult.TEST_CASE_RESULT_PASS) {
+                ++passCount;
+            } else if (result != VtsReportMessage.TestCaseResult.TEST_CASE_RESULT_SKIP) {
+                ++failCount;
+            }
+            if (testCase.getSystraceCount() > 0
+                    && testCase.getSystraceList().get(0).getUrlCount() > 0) {
+                String systraceLink = testCase.getSystraceList().get(0).getUrl(0).toStringUtf8();
+                linkList.add(systraceLink);
+            }
+
+            // Process coverage data for test case
+            for (VtsReportMessage.CoverageReportMessage coverage : testCase.getCoverageList()) {
+                CoverageEntity coverageEntity =
+                        CoverageEntity.fromCoverageReport(testRunKey, testCaseName, coverage);
+                if (coverageEntity == null) {
+                    log.warn("Invalid coverage report in test run " + testRunKey);
+                } else {
+                    coveredLineCount += coverageEntity.getCoveredCount();
+                    totalLineCount += coverageEntity.getTotalCount();
+                    coverageEntityList.add(coverageEntity);
+                }
+            }
+
+            // Process profiling data for test case
+            for (VtsReportMessage.ProfilingReportMessage profiling : testCase.getProfilingList()) {
+                ProfilingPointRunEntity profilingPointRunEntity =
+                        ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
+                if (profilingPointRunEntity == null) {
+                    log.warn("Invalid profiling report in test run " + testRunKey);
+                } else {
+                    profilingPointRunEntityList.add(profilingPointRunEntity);
+                    profilingPointKeyList.add(profilingPointRunEntity.getKey());
+                    testEntity.setHasProfilingData(true);
+                }
+            }
+
+            TestCaseRunEntity testCaseRunEntity = testCaseRunEntityList.get(testCaseRunEntityIndex);
+            if (!testCaseRunEntity.addTestCase(testCaseName, result.getNumber())) {
+                testCaseRunEntity = new TestCaseRunEntity();
+                testCaseRunEntity.addTestCase(testCaseName, result.getNumber());
+                testCaseRunEntityList.add(testCaseRunEntity);
+                testCaseRunEntityIndex++;
+            }
+        }
+
+        // Process device information
+        long testRunType = 0;
+        for (VtsReportMessage.AndroidDeviceInfoMessage device : report.getDeviceInfoList()) {
+            DeviceInfoEntity deviceInfoEntity =
+                    DeviceInfoEntity.fromDeviceInfoMessage(testRunKey, device);
+            if (deviceInfoEntity == null) {
+                log.warn("Invalid device info in test run " + testRunKey);
+            } else {
+                // Run type on devices must be the same, else set to OTHER
+                TestRunEntity.TestRunType runType =
+                        TestRunEntity.TestRunType.fromBuildId(deviceInfoEntity.getBuildId());
+                if (runType == null) {
+                    testRunType = TestRunEntity.TestRunType.OTHER.getNumber();
+                } else {
+                    testRunType = runType.getNumber();
+                }
+                deviceInfoEntityList.add(deviceInfoEntity);
+                BuildTargetEntity target = new BuildTargetEntity(deviceInfoEntity.getBuildFlavor());
+                if (buildTargetKeys.add(target.getKey())) {
+                    buildTargetEntityList.add(target);
+                }
+                BranchEntity branch = new BranchEntity(deviceInfoEntity.getBranch());
+                if (branchKeys.add(branch.getKey())) {
+                    branchEntityList.add(branch);
+                }
+            }
+        }
+
+        // Overall run type should be determined by the device builds unless test build is OTHER
+        if (testRunType == TestRunEntity.TestRunType.OTHER.getNumber()) {
+            testRunType = TestRunEntity.TestRunType.fromBuildId(testBuildId).getNumber();
+        } else if (TestRunEntity.TestRunType.fromBuildId(testBuildId)
+                == TestRunEntity.TestRunType.OTHER) {
+            testRunType = TestRunEntity.TestRunType.OTHER.getNumber();
+        }
+
+        // Process global coverage data
+        for (VtsReportMessage.CoverageReportMessage coverage : report.getCoverageList()) {
+            CoverageEntity coverageEntity =
+                    CoverageEntity.fromCoverageReport(testRunKey, new String(), coverage);
+            if (coverageEntity == null) {
+                log.warn("Invalid coverage report in test run " + testRunKey);
+            } else {
+                coveredLineCount += coverageEntity.getCoveredCount();
+                totalLineCount += coverageEntity.getTotalCount();
+                coverageEntityList.add(coverageEntity);
+            }
+        }
+
+        // Process global API coverage data
+        for (VtsReportMessage.ApiCoverageReportMessage apiCoverage : report.getApiCoverageList()) {
+            VtsReportMessage.HalInterfaceMessage halInterfaceMessage =
+                    apiCoverage.getHalInterface();
+            List<String> halApiList =
+                    apiCoverage
+                            .getHalApiList()
+                            .stream()
+                            .map(h -> h.toStringUtf8())
+                            .collect(Collectors.toList());
+            List<String> coveredHalApiList =
+                    apiCoverage
+                            .getCoveredHalApiList()
+                            .stream()
+                            .map(h -> h.toStringUtf8())
+                            .collect(Collectors.toList());
+            ApiCoverageEntity apiCoverageEntity =
+                    new ApiCoverageEntity(
+                            testRunKey,
+                            halInterfaceMessage.getHalPackageName().toStringUtf8(),
+                            halInterfaceMessage.getHalVersionMajor(),
+                            halInterfaceMessage.getHalVersionMinor(),
+                            halInterfaceMessage.getHalInterfaceName().toStringUtf8(),
+                            halApiList,
+                            coveredHalApiList);
+            apiCoverageEntityList.add(apiCoverageEntity);
+        }
+
+        // Process global profiling data
+        for (VtsReportMessage.ProfilingReportMessage profiling : report.getProfilingList()) {
+            ProfilingPointRunEntity profilingPointRunEntity =
+                    ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
+            if (profilingPointRunEntity == null) {
+                log.warn("Invalid profiling report in test run " + testRunKey);
+            } else {
+                profilingPointRunEntityList.add(profilingPointRunEntity);
+                profilingPointKeyList.add(profilingPointRunEntity.getKey());
+                testEntity.setHasProfilingData(true);
+            }
+        }
+
+        // Process log data
+        for (VtsReportMessage.LogMessage log : report.getLogList()) {
+            if (log.hasUrl()) {
+                linkList.add(log.getUrl().toStringUtf8());
+            }
+        }
+        // Process url resource
+        for (VtsReportMessage.UrlResourceMessage resource : report.getLinkResourceList()) {
+            if (resource.hasUrl()) {
+                linkList.add(resource.getUrl().toStringUtf8());
+            }
+        }
+
+        boolean hasCodeCoverage = totalLineCount > 0 && coveredLineCount >= 0;
+        TestRunEntity testRunEntity =
+                new TestRunEntity(
+                        testEntity.getOldKey(),
+                        testRunType,
+                        startTimestamp,
+                        endTimestamp,
+                        testBuildId,
+                        hostName,
+                        passCount,
+                        failCount,
+                        hasCodeCoverage,
+                        new ArrayList<>(),
+                        linkList);
+        testRunEntityList.add(testRunEntity);
+
+        CodeCoverageEntity codeCoverageEntity =
+                new CodeCoverageEntity(
+                        testRunEntity.getId(),
+                        testRunEntity.getKey(),
+                        coveredLineCount,
+                        totalLineCount);
+        codeCoverageEntityList.add(codeCoverageEntity);
+
+        ofy().transact(
+                        () -> {
+                            List<Long> testCaseIds = new ArrayList<>();
+                            for (Object entity : allEntityList) {
+                                if (entity instanceof List) {
+                                    List listEntity = (List) entity;
+                                    if (listEntity.size() > 0
+                                            && listEntity.get(0) instanceof TestCaseRunEntity) {
+                                        List<TestCaseRunEntity> dashboardEntityList =
+                                                (List<TestCaseRunEntity>) entity;
+                                        Map<
+                                                        com.googlecode.objectify.Key<
+                                                                TestCaseRunEntity>,
+                                                        TestCaseRunEntity>
+                                                testCaseRunEntityMap =
+                                                        DashboardEntity.saveAll(
+                                                                dashboardEntityList,
+                                                                this
+                                                                        .MAX_ENTITY_SIZE_PER_TRANSACTION);
+
+                                        testCaseIds =
+                                                testCaseRunEntityMap
+                                                        .values()
+                                                        .stream()
+                                                        .map(
+                                                                testCaseRunEntity ->
+                                                                        testCaseRunEntity.getId())
+                                                        .collect(Collectors.toList());
+                                    } else if (listEntity.size() > 0
+                                            && listEntity.get(0) instanceof TestRunEntity) {
+                                        List<TestRunEntity> dashboardEntityList =
+                                                (List<TestRunEntity>) entity;
+                                        dashboardEntityList.get(0).setTestCaseIds(testCaseIds);
+                                        DashboardEntity.saveAll(
+                                                dashboardEntityList,
+                                                this.MAX_ENTITY_SIZE_PER_TRANSACTION);
+                                    } else {
+                                        List<DashboardEntity> dashboardEntityList =
+                                                (List<DashboardEntity>) entity;
+                                        DashboardEntity.saveAll(
+                                                dashboardEntityList,
+                                                this.MAX_ENTITY_SIZE_PER_TRANSACTION);
+                                    }
+                                }
+                            }
+                        });
+    }
+
+    /**
+     * Upload data from a test plan report message
+     *
+     * @param report The test plan report containing data to upload.
+     */
+    private void insertTestPlanReport(TestPlanReportMessage report) {
+        List<DeviceInfoEntity> deviceInfoEntityList = new ArrayList<>();
+        List<HalApiEntity> halApiEntityList = new ArrayList<>();
+
+        List allEntityList = Arrays.asList(deviceInfoEntityList, halApiEntityList);
+
+        List<String> testModules = report.getTestModuleNameList();
+        List<Long> testTimes = report.getTestModuleStartTimestampList();
+        if (testModules.size() != testTimes.size() || !report.hasTestPlanName()) {
+            log.error("TestPlanReportMessage is missing information.");
+            return;
+        }
+
+        String testPlanName = report.getTestPlanName();
+        TestPlanEntity testPlanEntity = new TestPlanEntity(testPlanName);
+        List<com.googlecode.objectify.Key<TestRunEntity>> testRunKeyList = new ArrayList<>();
+        for (int index = 0; index < testModules.size(); index++) {
+            String test = testModules.get(index);
+            long time = testTimes.get(index);
+            com.googlecode.objectify.Key testKey =
+                    com.googlecode.objectify.Key.create(TestEntity.class, test);
+            com.googlecode.objectify.Key testRunKey =
+                    com.googlecode.objectify.Key.create(testKey, TestRunEntity.class, time);
+            testRunKeyList.add(testRunKey);
+        }
+
+        Map<com.googlecode.objectify.Key<TestRunEntity>, TestRunEntity> testRunEntityMap =
+                ofy().load().keys(() -> testRunKeyList.iterator());
+
+        testRunKeyList.forEach(
+                (v) -> {
+                    log.debug("TestRunEntity key value => " + v);
+                });
+        log.debug("testRunEntityMap value => " + testRunEntityMap.values());
+        log.debug("testRunEntityMap keySet => " + testRunEntityMap.keySet());
+
+        long passCount = 0;
+        long failCount = 0;
+        long startTimestamp = -1;
+        long endTimestamp = -1;
+        String testBuildId = null;
+        long testType = -1;
+        Set<DeviceInfoEntity> deviceInfoEntitySet = new HashSet<>();
+        for (TestRunEntity testRunEntity : testRunEntityMap.values()) {
+            passCount += testRunEntity.getPassCount();
+            failCount += testRunEntity.getFailCount();
+            if (startTimestamp < 0 || testRunEntity.getStartTimestamp() < startTimestamp) {
+                startTimestamp = testRunEntity.getStartTimestamp();
+            }
+            if (endTimestamp < 0 || testRunEntity.getEndTimestamp() > endTimestamp) {
+                endTimestamp = testRunEntity.getEndTimestamp();
+            }
+            testType = testRunEntity.getType();
+            testBuildId = testRunEntity.getTestBuildId();
+
+            List<DeviceInfoEntity> deviceInfoEntityListWithTestRunKey =
+                    ofy().load()
+                            .type(DeviceInfoEntity.class)
+                            .ancestor(testRunEntity.getOfyKey())
+                            .list();
+
+            for (DeviceInfoEntity deviceInfoEntity : deviceInfoEntityListWithTestRunKey) {
+                deviceInfoEntitySet.add(deviceInfoEntity);
+            }
+        }
+
+        if (startTimestamp < 0 || testBuildId == null || testType == -1) {
+            log.debug("startTimestamp => " + startTimestamp);
+            log.debug("testBuildId => " + testBuildId);
+            log.debug("type => " + testType);
+            log.error("Couldn't infer test run information from runs.");
+            return;
+        }
+
+        TestPlanRunEntity testPlanRunEntity =
+                new TestPlanRunEntity(
+                        testPlanEntity.getKey(),
+                        testPlanName,
+                        testType,
+                        startTimestamp,
+                        endTimestamp,
+                        testBuildId,
+                        passCount,
+                        failCount,
+                        0L,
+                        0L,
+                        testRunKeyList);
+
+        // Create the device infos.
+        for (DeviceInfoEntity device : deviceInfoEntitySet) {
+            deviceInfoEntityList.add(device.copyWithParent(testPlanRunEntity.getOfyKey()));
+        }
+
+        // Process global HAL API coverage data
+        for (VtsReportMessage.ApiCoverageReportMessage apiCoverage : report.getHalApiReportList()) {
+            VtsReportMessage.HalInterfaceMessage halInterfaceMessage =
+                    apiCoverage.getHalInterface();
+            List<String> halApiList =
+                    apiCoverage
+                            .getHalApiList()
+                            .stream()
+                            .map(h -> h.toStringUtf8())
+                            .collect(Collectors.toList());
+            List<String> coveredHalApiList =
+                    apiCoverage
+                            .getCoveredHalApiList()
+                            .stream()
+                            .map(h -> h.toStringUtf8())
+                            .collect(Collectors.toList());
+            HalApiEntity halApiEntity =
+                    new HalApiEntity(
+                            testPlanRunEntity.getOfyKey(),
+                            halInterfaceMessage.getHalReleaseLevel().toStringUtf8(),
+                            halInterfaceMessage.getHalPackageName().toStringUtf8(),
+                            halInterfaceMessage.getHalVersionMajor(),
+                            halInterfaceMessage.getHalVersionMinor(),
+                            halInterfaceMessage.getHalInterfaceName().toStringUtf8(),
+                            halApiList,
+                            coveredHalApiList);
+            halApiEntityList.add(halApiEntity);
+        }
+
+        ofy().transact(
+                        () -> {
+                            testPlanEntity.save();
+                            testPlanRunEntity.save();
+                            for (Object entity : allEntityList) {
+                                List<DashboardEntity> dashboardEntityList =
+                                        (List<DashboardEntity>) entity;
+                                Map<com.googlecode.objectify.Key<DashboardEntity>, DashboardEntity>
+                                        mapInfo =
+                                                DashboardEntity.saveAll(
+                                                        dashboardEntityList,
+                                                        this.MAX_ENTITY_SIZE_PER_TRANSACTION);
+                            }
+                        });
+
+        // Add the task to calculate total number API list.
+        testPlanRunEntity.addCoverageApiTask();
+    }
 }
diff --git a/src/main/java/com/android/vts/api/TestDataForDevServlet.java b/src/main/java/com/android/vts/api/TestDataForDevServlet.java
index 7fc067f..2ed387b 100644
--- a/src/main/java/com/android/vts/api/TestDataForDevServlet.java
+++ b/src/main/java/com/android/vts/api/TestDataForDevServlet.java
@@ -490,14 +490,14 @@
                                                     BuildTargetEntity buildTargetEntity =
                                                             new BuildTargetEntity(
                                                                     buildTarget.targetName);
-                                                    datastore.put(buildTargetEntity.toEntity());
+                                                    buildTargetEntity.save();
                                                 });
 
                                         testRun.branchList.forEach(
                                                 branch -> {
                                                     BranchEntity branchEntity =
                                                             new BranchEntity(branch.branchName);
-                                                    datastore.put(branchEntity.toEntity());
+                                                    branchEntity.save();
                                                 });
 
                                         boolean hasCodeCoverage =
@@ -630,13 +630,14 @@
                                             testBuildId,
                                             passCount,
                                             failCount,
-                                        0L,
-                                        0L,
+                                            0L,
+                                            0L,
                                             testRunKeys);
 
                             // Create the device infos.
                             for (DeviceInfoEntity device : devices) {
-                                datastore.put(device.copyWithParent(testPlanRun.key).toEntity());
+                                datastore.put(
+                                        device.copyWithParent(testPlanRun.getOfyKey()).toEntity());
                             }
                             datastore.put(testPlanRun.toEntity());
 
diff --git a/src/main/java/com/android/vts/config/ObjectifyListener.java b/src/main/java/com/android/vts/config/ObjectifyListener.java
index 9c4a705..5f35abd 100644
--- a/src/main/java/com/android/vts/config/ObjectifyListener.java
+++ b/src/main/java/com/android/vts/config/ObjectifyListener.java
@@ -23,6 +23,7 @@
 import com.android.vts.entity.CodeCoverageEntity;
 import com.android.vts.entity.CoverageEntity;
 import com.android.vts.entity.DeviceInfoEntity;
+import com.android.vts.entity.HalApiEntity;
 import com.android.vts.entity.ProfilingPointEntity;
 import com.android.vts.entity.ProfilingPointRunEntity;
 import com.android.vts.entity.ProfilingPointSummaryEntity;
@@ -87,6 +88,7 @@
         ObjectifyService.register(BranchEntity.class);
         ObjectifyService.register(BuildTargetEntity.class);
 
+        ObjectifyService.register(HalApiEntity.class);
         ObjectifyService.register(ApiCoverageEntity.class);
         ObjectifyService.register(ApiCoverageExcludedEntity.class);
         ObjectifyService.register(CodeCoverageEntity.class);
@@ -106,6 +108,7 @@
         ObjectifyService.register(TestStatusEntity.class);
         ObjectifyService.register(TestSuiteFileEntity.class);
         ObjectifyService.register(TestSuiteResultEntity.class);
+        ObjectifyService.register(TestAcknowledgmentEntity.class);
         ObjectifyService.register(RoleEntity.class);
         ObjectifyService.register(UserEntity.class);
         ObjectifyService.begin();
@@ -123,6 +126,9 @@
 
             servletContextEvent
                     .getServletContext()
+                    .setAttribute("systemConfigProp", systemConfigProp);
+            servletContextEvent
+                    .getServletContext()
                     .setAttribute("dataStoreFactory", DATA_STORE_FACTORY);
             servletContextEvent
                     .getServletContext()
diff --git a/src/main/java/com/android/vts/entity/ApiCoverageEntity.java b/src/main/java/com/android/vts/entity/ApiCoverageEntity.java
index 0a41743..9c74032 100644
--- a/src/main/java/com/android/vts/entity/ApiCoverageEntity.java
+++ b/src/main/java/com/android/vts/entity/ApiCoverageEntity.java
@@ -35,167 +35,125 @@
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 
-/**
- * Entity Class for ApiCoverageEntity
- */
+/** Entity Class for ApiCoverageEntity */
 @Cache
 @Entity(name = "ApiCoverage")
 @EqualsAndHashCode(of = "id")
 @NoArgsConstructor
 @JsonAutoDetect(fieldVisibility = Visibility.ANY)
 @JsonIgnoreProperties({"id", "parent"})
-public class ApiCoverageEntity {
+public class ApiCoverageEntity implements DashboardEntity {
 
-  /**
-   * ApiCoverageEntity id field
-   */
-  @Id
-  @Getter
-  @Setter
-  String id;
+    /** ApiCoverageEntity id field */
+    @Id @Getter @Setter String id;
 
-  @Parent
-  @Getter
-  Key<?> parent;
+    @Parent @Getter Key<?> parent;
 
-  /**
-   * HAL package name. e.g. android.hardware.foo.
-   */
-  @Index
-  @Getter
-  @Setter
-  String halPackageName;
+    /** HAL package name. e.g. android.hardware.foo. */
+    @Index @Getter @Setter String halPackageName;
 
-  /**
-   * HAL (major) version. e.g. 1.
-   */
-  @Index
-  @Getter
-  @Setter
-  int halMajorVersion;
+    /** HAL (major) version. e.g. 1. */
+    @Index @Getter @Setter int halMajorVersion;
 
-  /**
-   * HAL (minor) version. e.g. 0.
-   */
-  @Index
-  @Getter
-  @Setter
-  int halMinorVersion;
+    /** HAL (minor) version. e.g. 0. */
+    @Index @Getter @Setter int halMinorVersion;
 
-  /**
-   * HAL interface name. e.g. IFoo.
-   */
-  @Index
-  @Getter
-  @Setter
-  String halInterfaceName;
+    /** HAL interface name. e.g. IFoo. */
+    @Index @Getter @Setter String halInterfaceName;
 
-  /**
-   * List of HAL API
-   */
-  @Getter
-  @Setter
-  List<String> halApi;
+    /** List of HAL API */
+    @Getter @Setter List<String> halApi;
 
-  /**
-   * List of HAL covered API
-   */
-  @Getter
-  @Setter
-  List<String> coveredHalApi;
+    /** List of HAL covered API */
+    @Getter @Setter List<String> coveredHalApi;
 
-  /**
-   * When this record was created or updated
-   */
-  @Index
-  Date updated;
+    /** When this record was created or updated */
+    @Index Date updated;
 
-  /**
-   * Constructor function for ApiCoverageEntity Class
-   */
-  public ApiCoverageEntity(com.google.appengine.api.datastore.Key testRunKey, String halPackageName,
-      int halVersionMajor, int halVersionMinor, String halInterfaceName, List<String> halApi,
-      List<String> coveredHalApi) {
+    /** Constructor function for ApiCoverageEntity Class */
+    public ApiCoverageEntity(
+            com.google.appengine.api.datastore.Key testRunKey,
+            String halPackageName,
+            int halVersionMajor,
+            int halVersionMinor,
+            String halInterfaceName,
+            List<String> halApi,
+            List<String> coveredHalApi) {
+        this.id = UUID.randomUUID().toString();
+        this.parent = getParentKey(testRunKey);
 
-    this.parent = getParentKey(testRunKey);
+        this.halPackageName = halPackageName;
+        this.halMajorVersion = halVersionMajor;
+        this.halMinorVersion = halVersionMinor;
+        this.halInterfaceName = halInterfaceName;
+        this.halApi = halApi;
+        this.coveredHalApi = coveredHalApi;
+        this.updated = new Date();
+    }
 
-    this.halPackageName = halPackageName;
-    this.halMajorVersion = halVersionMajor;
-    this.halMinorVersion = halVersionMinor;
-    this.halInterfaceName = halInterfaceName;
-    this.halApi = halApi;
-    this.coveredHalApi = coveredHalApi;
-  }
+    /** Constructor function for ApiCoverageEntity Class with objectify Key. */
+    public ApiCoverageEntity(
+            Key testRunKey,
+            String halPackageName,
+            int halVersionMajor,
+            int halVersionMinor,
+            String halInterfaceName,
+            List<String> halApi,
+            List<String> coveredHalApi) {
+        this.id = UUID.randomUUID().toString();
+        this.parent = testRunKey;
 
-  /**
-   * Constructor function for ApiCoverageEntity Class with objectify Key.
-   */
-  public ApiCoverageEntity(Key testRunKey, String halPackageName,
-                           int halVersionMajor, int halVersionMinor, String halInterfaceName,
-                           List<String> halApi, List<String> coveredHalApi) {
-    this.parent = testRunKey;
+        this.halPackageName = halPackageName;
+        this.halMajorVersion = halVersionMajor;
+        this.halMinorVersion = halVersionMinor;
+        this.halInterfaceName = halInterfaceName;
+        this.halApi = halApi;
+        this.coveredHalApi = coveredHalApi;
+        this.updated = new Date();
+    }
 
-    this.halPackageName = halPackageName;
-    this.halMajorVersion = halVersionMajor;
-    this.halMinorVersion = halVersionMinor;
-    this.halInterfaceName = halInterfaceName;
-    this.halApi = halApi;
-    this.coveredHalApi = coveredHalApi;
-  }
+    /** Get objectify Key from datastore Key type */
+    private Key getParentKey(com.google.appengine.api.datastore.Key testRunKey) {
+        Key testParentKey = Key.create(TestEntity.class, testRunKey.getParent().getName());
+        return Key.create(testParentKey, TestRunEntity.class, testRunKey.getId());
+    }
 
-  /**
-   * Get objectify Key from datastore Key type
-   */
-  private Key getParentKey(com.google.appengine.api.datastore.Key testRunKey) {
-    Key testParentKey = Key.create(TestEntity.class, testRunKey.getParent().getName());
-    return Key.create(testParentKey, TestRunEntity.class, testRunKey.getId());
-  }
+    /** Get UrlSafeKey from ApiCoverageEntity Information */
+    public String getUrlSafeKey() {
+        Key uuidKey = Key.create(this.parent, ApiCoverageEntity.class, this.id);
+        return uuidKey.toUrlSafe();
+    }
 
-  /**
-   * Get UrlSafeKey from ApiCoverageEntity Information
-   */
-  public String getUrlSafeKey() {
-    Key uuidKey = Key.create(this.parent, ApiCoverageEntity.class, this.id);
-    return uuidKey.toUrlSafe();
-  }
+    /** Saving function for the instance of this class */
+    @Override
+    public Key<ApiCoverageEntity> save() {
+        this.id = UUID.randomUUID().toString();
+        this.updated = new Date();
+        return ofy().save().entity(this).now();
+    }
 
-  /**
-   * Saving function for the instance of this class
-   */
-  public Key<ApiCoverageEntity> save() {
-    this.id = UUID.randomUUID().toString();
-    this.updated = new Date();
-    return ofy().save().entity(this).now();
-  }
+    /** Get List of ApiCoverageEntity by HAL interface name */
+    public static ApiCoverageEntity getByUrlSafeKey(String urlSafeKey) {
+        return ofy().load()
+                .type(ApiCoverageEntity.class)
+                .filterKey(com.google.cloud.datastore.Key.fromUrlSafe(urlSafeKey))
+                .first()
+                .now();
+    }
 
-  /**
-   * Get List of ApiCoverageEntity by HAL interface name
-   */
-  public static ApiCoverageEntity getByUrlSafeKey(String urlSafeKey) {
-    return ofy().load()
-        .type(ApiCoverageEntity.class)
-        .filterKey(com.google.cloud.datastore.Key.fromUrlSafe(urlSafeKey))
-        .first()
-        .now();
-  }
+    /** Get List of ApiCoverageEntity by HAL interface name */
+    public static List<ApiCoverageEntity> getByInterfaceNameList(String halInterfaceName) {
+        return ofy().load()
+                .type(ApiCoverageEntity.class)
+                .filter("halInterfaceName", halInterfaceName)
+                .list();
+    }
 
-  /**
-   * Get List of ApiCoverageEntity by HAL interface name
-   */
-  public static List<ApiCoverageEntity> getByInterfaceNameList(String halInterfaceName) {
-    return ofy().load()
-        .type(ApiCoverageEntity.class)
-        .filter("halInterfaceName", halInterfaceName)
-        .list();
-  }
-
-  /**
-   * Get List of ApiCoverageEntity by HAL package name
-   */
-  public static List<ApiCoverageEntity> getByPackageNameList(String packageName) {
-    return ofy().load()
-        .type(ApiCoverageEntity.class)
-        .filter("halPackageName", packageName)
-        .list();
-  }
+    /** Get List of ApiCoverageEntity by HAL package name */
+    public static List<ApiCoverageEntity> getByPackageNameList(String packageName) {
+        return ofy().load()
+                .type(ApiCoverageEntity.class)
+                .filter("halPackageName", packageName)
+                .list();
+    }
 }
diff --git a/src/main/java/com/android/vts/entity/ApiCoverageExcludedEntity.java b/src/main/java/com/android/vts/entity/ApiCoverageExcludedEntity.java
index 834f8cc..61bd6a7 100644
--- a/src/main/java/com/android/vts/entity/ApiCoverageExcludedEntity.java
+++ b/src/main/java/com/android/vts/entity/ApiCoverageExcludedEntity.java
@@ -28,11 +28,8 @@
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 
-import java.util.Collection;
 import java.util.Date;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 import static com.googlecode.objectify.ObjectifyService.ofy;
 
@@ -46,10 +43,7 @@
 @NoArgsConstructor
 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
 @JsonIgnoreProperties({"id", "parent"})
-public class ApiCoverageExcludedEntity {
-
-    // The maximum number of entity list size to insert datastore
-    private static final int maxNumEntitySize = 500;
+public class ApiCoverageExcludedEntity implements DashboardEntity {
 
     /** ApiCoverageEntity id field */
     @Id @Getter @Setter private String id;
@@ -82,11 +76,12 @@
             String interfaceName,
             String apiName,
             String comment) {
-
+        this.id = this.getObjectifyId();
         this.packageName = packageName;
         this.interfaceName = interfaceName;
         this.apiName = apiName;
         this.comment = comment;
+        this.updated = new Date();
 
         this.setVersions(version);
     }
@@ -125,44 +120,14 @@
     }
 
     /** Saving function for the instance of this class */
+    @Override
     public Key<ApiCoverageExcludedEntity> save() {
-        this.id = this.getObjectifyId();
-        this.updated = new Date();
         return ofy().save().entity(this).now();
     }
 
-    /** Spliting a list based on a given size */
-    public static <T> Collection<List<T>> partitionBasedOnSize(List<T> inputList, int size) {
-        final AtomicInteger counter = new AtomicInteger(0);
-        return inputList
-                .stream()
-                .collect(Collectors.groupingBy(s -> counter.getAndIncrement() / size))
-                .values();
-    }
-
-    /** Saving function with parameter of this entity List */
-    public static void saveAll(List<ApiCoverageExcludedEntity> apiCoverageExcludedEntityList) {
-        List<ApiCoverageExcludedEntity> entityWithIdList =
-                apiCoverageExcludedEntityList
-                        .stream()
-                        .map(
-                                entity -> {
-                                    entity.setId(entity.getObjectifyId());
-                                    entity.setUpdated(new Date());
-                                    return entity;
-                                })
-                        .collect(Collectors.toList());
-
-        partitionBasedOnSize(entityWithIdList, maxNumEntitySize)
-                .stream()
-                .forEach(
-                        entityList -> {
-                            ofy().save().entities(entityList).now();
-                        });
-    }
-
     /** Get All Key List of ApiCoverageExcludedEntity */
     public static List<Key<ApiCoverageExcludedEntity>> getAllKeyList() {
         return ofy().load().type(ApiCoverageExcludedEntity.class).keys().list();
     }
+
 }
diff --git a/src/main/java/com/android/vts/entity/BranchEntity.java b/src/main/java/com/android/vts/entity/BranchEntity.java
index fa3f7a5..1c9ea52 100644
--- a/src/main/java/com/android/vts/entity/BranchEntity.java
+++ b/src/main/java/com/android/vts/entity/BranchEntity.java
@@ -41,8 +41,6 @@
 
     public static final String KIND = "Branch"; // The entity kind.
 
-    public Key key; // The key for the entity in the database.
-
     @Id private String name;
 
     /**
@@ -51,7 +49,11 @@
      * @param branchName The name of the branch.
      */
     public BranchEntity(String branchName) {
-        this.key = KeyFactory.createKey(KIND, branchName);
+        this.name = branchName;
+    }
+
+    public Key getKey() {
+        return KeyFactory.createKey(KIND, this.name);
     }
 
     /** find by branch name */
@@ -86,9 +88,10 @@
         }
     }
 
+    /** Saving function for the instance of this class */
     @Override
-    public Entity toEntity() {
-        return new Entity(this.key);
+    public com.googlecode.objectify.Key<BranchEntity> save() {
+        return ofy().save().entity(this).now();
     }
 
     /**
diff --git a/src/main/java/com/android/vts/entity/BuildTargetEntity.java b/src/main/java/com/android/vts/entity/BuildTargetEntity.java
index b204d0c..4180213 100644
--- a/src/main/java/com/android/vts/entity/BuildTargetEntity.java
+++ b/src/main/java/com/android/vts/entity/BuildTargetEntity.java
@@ -41,8 +41,6 @@
 
     public static final String KIND = "BuildTarget"; // The entity kind.
 
-    public Key key; // The key for the entity in the database.
-
     @Id private String name;
 
     /**
@@ -51,12 +49,16 @@
      * @param targetName The name of the build target.
      */
     public BuildTargetEntity(String targetName) {
-        this.key = KeyFactory.createKey(KIND, targetName);
+        this.name = targetName;
     }
 
+    public Key getKey() {
+        return KeyFactory.createKey(KIND, this.name);
+    }
+    /** Saving function for the instance of this class */
     @Override
-    public Entity toEntity() {
-        return new Entity(this.key);
+    public com.googlecode.objectify.Key<BuildTargetEntity> save() {
+        return ofy().save().entity(this).now();
     }
 
     /** find by Build Target Name */
diff --git a/src/main/java/com/android/vts/entity/CodeCoverageEntity.java b/src/main/java/com/android/vts/entity/CodeCoverageEntity.java
index 63034ef..2a46b1f 100644
--- a/src/main/java/com/android/vts/entity/CodeCoverageEntity.java
+++ b/src/main/java/com/android/vts/entity/CodeCoverageEntity.java
@@ -16,13 +16,10 @@
 
 package com.android.vts.entity;
 
-import com.android.vts.util.UrlUtil;
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
 import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.google.appengine.api.datastore.KeyFactory;
-import com.google.gson.Gson;
-import com.google.gson.JsonElement;
 import com.googlecode.objectify.Key;
 
 import com.googlecode.objectify.annotation.Cache;
@@ -35,10 +32,7 @@
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 
-import java.util.ArrayList;
 import java.util.Date;
-import java.util.List;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import static com.googlecode.objectify.ObjectifyService.ofy;
@@ -50,7 +44,7 @@
 @NoArgsConstructor
 @JsonAutoDetect(fieldVisibility = Visibility.ANY)
 @JsonIgnoreProperties({"id", "parent"})
-public class CodeCoverageEntity {
+public class CodeCoverageEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(CodeCoverageEntity.class.getName());
 
     public static final String KIND = "CodeCoverage";
@@ -59,7 +53,7 @@
     public static final String TOTAL_LINE_COUNT = "totalLineCount";
 
     /** CodeCoverageEntity id field */
-    @Id @Getter @Setter long id;
+    @Id @Getter @Setter Long id;
 
     @Parent @Getter Key<?> parent;
 
@@ -82,6 +76,20 @@
         this.totalLineCount = totalLineCount;
     }
 
+    /** Constructor function for ApiCoverageEntity Class */
+    public CodeCoverageEntity(
+            long id,
+            com.google.appengine.api.datastore.Key testRunKey,
+            long coveredLineCount,
+            long totalLineCount) {
+        this.id = id;
+
+        this.parent = getParentKey(testRunKey);
+
+        this.coveredLineCount = coveredLineCount;
+        this.totalLineCount = totalLineCount;
+    }
+
     /** Constructor function for ApiCoverageEntity Class with objectify key*/
     public CodeCoverageEntity(Key testRunKey, long coveredLineCount, long totalLineCount) {
         this.parent = testRunKey;
@@ -102,6 +110,7 @@
     }
 
     /** Saving function for the instance of this class */
+    @Override
     public Key<CodeCoverageEntity> save() {
         this.id = this.getParent().getId();
         this.updated = new Date();
diff --git a/src/main/java/com/android/vts/entity/CodeCoverageFileEntity.java b/src/main/java/com/android/vts/entity/CodeCoverageFileEntity.java
index cb6d06c..fe4e8a0 100644
--- a/src/main/java/com/android/vts/entity/CodeCoverageFileEntity.java
+++ b/src/main/java/com/android/vts/entity/CodeCoverageFileEntity.java
@@ -37,11 +37,10 @@
 @Entity(name = "CodeCoverageFile")
 @EqualsAndHashCode(of = "id")
 @NoArgsConstructor
-public class CodeCoverageFileEntity {
+public class CodeCoverageFileEntity implements DashboardEntity {
 
     /** CodeCoverageFileEntity testName field */
-    @Id
-    @Getter @Setter long id;
+    @Id @Getter @Setter Long id;
 
     @Parent
     @Getter @Setter private Key<?> coverageParent;
@@ -94,8 +93,9 @@
     }
 
     /** Saving function for the instance of this class */
-    public void save() {
+    @Override
+    public Key<CodeCoverageFileEntity> save() {
         this.isIgnored = false;
-        ofy().save().entity(this).now();
+        return ofy().save().entity(this).now();
     }
 }
diff --git a/src/main/java/com/android/vts/entity/CoverageEntity.java b/src/main/java/com/android/vts/entity/CoverageEntity.java
index 82b6690..50d2f4e 100644
--- a/src/main/java/com/android/vts/entity/CoverageEntity.java
+++ b/src/main/java/com/android/vts/entity/CoverageEntity.java
@@ -21,14 +21,11 @@
 import com.android.vts.proto.VtsReportMessage.CoverageReportMessage;
 import com.google.appengine.api.datastore.Entity;
 import com.google.appengine.api.datastore.Key;
-import com.google.cloud.datastore.PathElement;
-import com.googlecode.objectify.LoadResult;
 import com.googlecode.objectify.annotation.Cache;
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Index;
 import com.googlecode.objectify.annotation.Parent;
-import java.io.Serializable;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.util.ArrayList;
@@ -48,231 +45,268 @@
 @Data
 @NoArgsConstructor
 /** Object describing coverage data gathered for a file. */
-public class CoverageEntity implements Serializable {
+public class CoverageEntity implements DashboardEntity {
 
-  protected static final Logger logger = Logger.getLogger(CoverageEntity.class.getName());
+    protected static final Logger logger = Logger.getLogger(CoverageEntity.class.getName());
 
-  public static final String KIND = "Coverage";
+    public static final String KIND = "Coverage";
 
-  public static String GERRIT_URI;
+    public static String GERRIT_URI;
 
-  // 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";
+    // 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";
 
-  @Ignore
-  @Getter
-  @Setter
-  private Key parentKey;
+    @Ignore @Getter @Setter private Key parentKey;
 
-  @Id
-  @Getter
-  @Setter
-  private Long id;
+    @Id @Getter @Setter private Long id;
 
-  @Parent
-  @Getter
-  @Setter
-  private com.googlecode.objectify.Key<?> testParent;
+    @Parent @Getter @Setter private com.googlecode.objectify.Key<?> testParent;
 
-  @Index
-  @Getter
-  @Setter
-  private String group;
+    @Index @Getter @Setter private String group;
 
-  @Getter
-  @Setter
-  private long coveredCount;
+    @Getter @Setter private long coveredCount;
 
-  @Getter
-  @Setter
-  private long totalCount;
+    @Getter @Setter private long totalCount;
 
-  @Index
-  @Getter
-  @Setter
-  private String filePath;
+    @Index @Getter @Setter private String filePath;
 
-  @Getter
-  @Setter
-  private String projectName;
+    @Getter @Setter private String projectName;
 
-  @Getter
-  @Setter
-  private String projectVersion;
+    @Getter @Setter private String projectVersion;
 
-  @Getter
-  @Setter
-  private List<Long> lineCoverage;
+    @Getter @Setter private List<Long> lineCoverage;
 
-  /**
-   * CoverageEntity isIgnored field
-   */
-  @Index
-  @Getter
-  @Setter
-  Boolean isIgnored;
+    /** CoverageEntity isIgnored field */
+    @Index @Getter @Setter Boolean isIgnored;
 
-  /**
-   * 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.coveredCount = coveredLineCount;
-    this.totalCount = totalLineCount;
-    this.filePath = filePath;
-    this.projectName = projectName;
-    this.projectVersion = projectVersion;
-    this.lineCoverage = lineCoverage;
-  }
-
-  /**
-   * find coverage entity by ID
-   */
-  public static CoverageEntity findById(String testName, String testRunId, String id) {
-    com.googlecode.objectify.Key testKey = com.googlecode.objectify.Key
-        .create(TestEntity.class, testName);
-    com.googlecode.objectify.Key testRunKey = com.googlecode.objectify.Key
-        .create(testKey, TestRunEntity.class, Long.parseLong(testRunId));
-    return ofy().load().type(CoverageEntity.class).parent(testRunKey).id(Long.parseLong(id)).now();
-  }
-
-  public static void setPropertyValues(Properties newSystemConfigProp) {
-    GERRIT_URI = newSystemConfigProp.getProperty("gerrit.uri");
-  }
-
-  /**
-   * Saving function for the instance of this class
-   */
-  public void save() {
-    ofy().save().entity(this).now();
-  }
-
-  /**
-   * Get percentage from calculating coveredCount and totalCount values
-   */
-  public Double getPercentage() {
-    return Math.round(coveredCount * 10000d / totalCount) / 100d;
-  }
-
-  /**
-   * Get Gerrit Url function from the attributes of this class
-   */
-  public String getGerritUrl() throws UnsupportedEncodingException {
-    String gerritPath = GERRIT_URI + "/projects/" +
-        URLEncoder.encode(projectName, "UTF-8") + "/commits/" +
-        URLEncoder.encode(projectVersion, "UTF-8") + "/files/" +
-        URLEncoder.encode(filePath, "UTF-8") + "/content";
-    return gerritPath;
-  }
-
-  /* Comparator for sorting the list by isIgnored field */
-  public static Comparator<CoverageEntity> isIgnoredComparator = new Comparator<CoverageEntity>() {
-
-    public int compare(CoverageEntity coverageEntity1, CoverageEntity coverageEntity2) {
-      Boolean isIgnored1 =
-          Objects.isNull(coverageEntity1.getIsIgnored()) ? false : coverageEntity1.getIsIgnored();
-      Boolean isIgnored2 =
-          Objects.isNull(coverageEntity2.getIsIgnored()) ? false : coverageEntity2.getIsIgnored();
-
-      // ascending order
-      return isIgnored1.compareTo(isIgnored2);
+    /**
+     * 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.coveredCount = coveredLineCount;
+        this.totalCount = totalLineCount;
+        this.filePath = filePath;
+        this.projectName = projectName;
+        this.projectVersion = projectVersion;
+        this.lineCoverage = lineCoverage;
     }
-  };
 
-  public Entity toEntity() {
-    Entity coverageEntity = new Entity(KIND, parentKey);
-    coverageEntity.setProperty(GROUP, group);
-    coverageEntity.setUnindexedProperty(COVERED_LINE_COUNT, coveredCount);
-    coverageEntity.setUnindexedProperty(TOTAL_LINE_COUNT, totalCount);
-    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);
+    /**
+     * Create a CoverageEntity object for a file.
+     *
+     * @param testParent The objectify 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(
+            com.googlecode.objectify.Key testParent,
+            String group,
+            long coveredLineCount,
+            long totalLineCount,
+            String filePath,
+            String projectName,
+            String projectVersion,
+            List<Long> lineCoverage) {
+        this.testParent = testParent;
+        this.group = group;
+        this.coveredCount = coveredLineCount;
+        this.totalCount = totalLineCount;
+        this.filePath = filePath;
+        this.projectName = projectName;
+        this.projectVersion = projectVersion;
+        this.lineCoverage = 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;
+    /** find coverage entity by ID */
+    public static CoverageEntity findById(String testName, String testRunId, String id) {
+        com.googlecode.objectify.Key testKey =
+                com.googlecode.objectify.Key.create(TestEntity.class, testName);
+        com.googlecode.objectify.Key testRunKey =
+                com.googlecode.objectify.Key.create(
+                        testKey, TestRunEntity.class, Long.parseLong(testRunId));
+        return ofy().load()
+                .type(CoverageEntity.class)
+                .parent(testRunKey)
+                .id(Long.parseLong(id))
+                .now();
     }
-    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;
+    public static void setPropertyValues(Properties newSystemConfigProp) {
+        GERRIT_URI = newSystemConfigProp.getProperty("gerrit.uri");
     }
-    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 (long count : coverage.getLineCoverageVectorList()) {
-        lineCoverage.add(count);
-      }
+
+    /** Saving function for the instance of this class */
+    @Override
+    public com.googlecode.objectify.Key<CoverageEntity> save() {
+        return ofy().save().entity(this).now();
     }
-    return new CoverageEntity(parentKey, group, coveredLineCount, totalLineCount, filePath,
-        projectName, projectVersion, lineCoverage);
-  }
+
+    /** Get percentage from calculating coveredCount and totalCount values */
+    public Double getPercentage() {
+        return Math.round(coveredCount * 10000d / totalCount) / 100d;
+    }
+
+    /** Get Gerrit Url function from the attributes of this class */
+    public String getGerritUrl() throws UnsupportedEncodingException {
+        String gerritPath =
+                GERRIT_URI
+                        + "/projects/"
+                        + URLEncoder.encode(projectName, "UTF-8")
+                        + "/commits/"
+                        + URLEncoder.encode(projectVersion, "UTF-8")
+                        + "/files/"
+                        + URLEncoder.encode(filePath, "UTF-8")
+                        + "/content";
+        return gerritPath;
+    }
+
+    /* Comparator for sorting the list by isIgnored field */
+    public static Comparator<CoverageEntity> isIgnoredComparator =
+            new Comparator<CoverageEntity>() {
+
+                public int compare(CoverageEntity coverageEntity1, CoverageEntity coverageEntity2) {
+                    Boolean isIgnored1 =
+                            Objects.isNull(coverageEntity1.getIsIgnored())
+                                    ? false
+                                    : coverageEntity1.getIsIgnored();
+                    Boolean isIgnored2 =
+                            Objects.isNull(coverageEntity2.getIsIgnored())
+                                    ? false
+                                    : coverageEntity2.getIsIgnored();
+
+                    // ascending order
+                    return isIgnored1.compareTo(isIgnored2);
+                }
+            };
+
+    public Entity toEntity() {
+        Entity coverageEntity = new Entity(KIND, parentKey);
+        coverageEntity.setProperty(GROUP, group);
+        coverageEntity.setUnindexedProperty(COVERED_LINE_COUNT, coveredCount);
+        coverageEntity.setUnindexedProperty(TOTAL_LINE_COUNT, totalCount);
+        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(
+            com.googlecode.objectify.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 (long count : coverage.getLineCoverageVectorList()) {
+                lineCoverage.add(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
index 402a1e5..a42c1eb 100644
--- a/src/main/java/com/android/vts/entity/DashboardEntity.java
+++ b/src/main/java/com/android/vts/entity/DashboardEntity.java
@@ -16,14 +16,41 @@
 
 package com.android.vts.entity;
 
-import com.google.appengine.api.datastore.Entity;
+import com.google.common.collect.Lists;
+import com.googlecode.objectify.Key;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.googlecode.objectify.ObjectifyService.ofy;
 
 /** Interface for interacting with VTS Dashboard entities in Cloud Datastore. */
-public interface DashboardEntity {
+public interface DashboardEntity extends Serializable {
     /**
-     * Serialize the DashboardEntity to an Entity object.
+     * Save the Entity to the datastore.
      *
-     * @return Entity object representing the properties defined in the DashboardEntity.
+     * @return The saved entity's key value.
      */
-    public Entity toEntity();
+    <T> Key<T> save();
+
+    /** Save List of entity through objectify entities method. */
+    static <T> Map<Key<T>, T> saveAll(List<T> entityList, int maxEntitySize) {
+        return ofy().transact(
+                        () -> {
+                            List<List<T>> partitionedList =
+                                    Lists.partition(entityList, maxEntitySize);
+                            return partitionedList
+                                    .stream()
+                                    .map(
+                                            subEntityList ->
+                                                    ofy().save().entities(subEntityList).now())
+                                    .flatMap(m -> m.entrySet().stream())
+                                    .collect(
+                                            Collectors.toMap(
+                                                    entry -> entry.getKey(),
+                                                    entry -> entry.getValue()));
+                        });
+    }
 }
diff --git a/src/main/java/com/android/vts/entity/DeviceInfoEntity.java b/src/main/java/com/android/vts/entity/DeviceInfoEntity.java
index d0c3d00..2b98355 100644
--- a/src/main/java/com/android/vts/entity/DeviceInfoEntity.java
+++ b/src/main/java/com/android/vts/entity/DeviceInfoEntity.java
@@ -27,7 +27,6 @@
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Index;
 import com.googlecode.objectify.annotation.Parent;
-import java.io.Serializable;
 import java.util.List;
 import java.util.Objects;
 import java.util.logging.Level;
@@ -44,7 +43,7 @@
 @Data
 @NoArgsConstructor
 /** Class describing a device used for a test run. */
-public class DeviceInfoEntity implements Serializable {
+public class DeviceInfoEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(DeviceInfoEntity.class.getName());
 
     /** This is the instance of App Engine memcache service java library */
@@ -64,8 +63,7 @@
     private Key parentKey;
 
     /** ID field using start timestamp */
-    @Id
-    private long id;
+    @Id private Long id;
 
     /** parent field based on Test and TestRun key */
     @Parent
@@ -87,15 +85,6 @@
 
     private String abiName;
 
-    /*
-    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.
      *
@@ -215,8 +204,8 @@
     }
 
     /** Saving function for the instance of this class */
-    public void save() {
-        ofy().save().entity(this).now();
+    public com.googlecode.objectify.Key<DeviceInfoEntity> save() {
+        return ofy().save().entity(this).now();
     }
 
     public Entity toEntity() {
@@ -270,12 +259,12 @@
     /**
      * Convert a device info message to a DeviceInfoEntity.
      *
-     * @param parentKey The ancestor key for the device entity.
+     * @param parent 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) {
+            com.googlecode.objectify.Key parent, AndroidDeviceInfoMessage device) {
         if (!device.hasBuildAlias() || !device.hasBuildFlavor() || !device.hasProductVariant()
                 || !device.hasBuildId()) {
             return null;
@@ -287,7 +276,7 @@
         String abiBitness = device.getAbiBitness().toStringUtf8();
         String abiName = device.getAbiName().toStringUtf8();
         return new DeviceInfoEntity(
-                parentKey, branch, product, buildFlavor, buildId, abiBitness, abiName);
+                parent, branch, product, buildFlavor, buildId, abiBitness, abiName);
     }
 
     @Override
@@ -312,10 +301,11 @@
 
     /**
      * 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) {
+    public DeviceInfoEntity copyWithParent(com.googlecode.objectify.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/HalApiEntity.java b/src/main/java/com/android/vts/entity/HalApiEntity.java
new file mode 100644
index 0000000..158a4ca
--- /dev/null
+++ b/src/main/java/com/android/vts/entity/HalApiEntity.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may
+ * obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.vts.entity;
+
+import static com.googlecode.objectify.ObjectifyService.ofy;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.googlecode.objectify.Key;
+import com.googlecode.objectify.annotation.Cache;
+import com.googlecode.objectify.annotation.Entity;
+import com.googlecode.objectify.annotation.Id;
+import com.googlecode.objectify.annotation.Index;
+import com.googlecode.objectify.annotation.Parent;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/** Entity Class for HalApiEntity */
+@Cache
+@Entity(name = "HalApiEntity")
+@EqualsAndHashCode(of = "id")
+@NoArgsConstructor
+@JsonAutoDetect(fieldVisibility = Visibility.ANY)
+@JsonIgnoreProperties({"id", "parent"})
+public class HalApiEntity implements DashboardEntity {
+
+    /** HalApiEntity id field */
+    @Id @Getter @Setter String id;
+
+    @Parent @Getter Key<?> parent;
+
+    /** HAL Api Release Level. e.g. */
+    @Index @Getter @Setter String halApiReleaseLevel;
+
+    /** HAL package name. e.g. android.hardware.foo. */
+    @Index @Getter @Setter String halPackageName;
+
+    /** HAL (major) version. e.g. 1. */
+    @Index @Getter @Setter int halVersionMajor;
+
+    /** HAL (minor) version. e.g. 0. */
+    @Index @Getter @Setter int halVersionMinor;
+
+    /** HAL interface name. e.g. IFoo. */
+    @Index @Getter @Setter String halInterfaceName;
+
+    /** List of HAL API */
+    @Getter @Setter List<String> halApi;
+
+    /** List of HAL covered API */
+    @Getter @Setter List<String> coveredHalApi;
+
+    /** When this record was created or updated */
+    @Index Date updated;
+
+    /** Constructor function for HalApiEntity Class */
+    public HalApiEntity(
+            com.googlecode.objectify.Key testRunKey,
+            String halApiReleaseLevel,
+            String halPackageName,
+            int halVersionMajor,
+            int halVersionMinor,
+            String halInterfaceName,
+            List<String> halApi,
+            List<String> coveredHalApi) {
+
+        this.id = UUID.randomUUID().toString();
+        this.parent = testRunKey;
+
+        this.halApiReleaseLevel = halApiReleaseLevel;
+        this.halPackageName = halPackageName;
+        this.halVersionMajor = halVersionMajor;
+        this.halVersionMinor = halVersionMinor;
+        this.halInterfaceName = halInterfaceName;
+        this.halApi = halApi;
+        this.coveredHalApi = coveredHalApi;
+        this.updated = new Date();
+    }
+
+    /** Saving function for the instance of this class */
+    @Override
+    public Key<HalApiEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointEntity.java
index 741a5a7..ac2994c 100644
--- a/src/main/java/com/android/vts/entity/ProfilingPointEntity.java
+++ b/src/main/java/com/android/vts/entity/ProfilingPointEntity.java
@@ -25,21 +25,20 @@
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Index;
-import java.io.Serializable;
 import java.util.Date;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import lombok.Data;
-import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
+
+import static com.googlecode.objectify.ObjectifyService.ofy;
 
 @com.googlecode.objectify.annotation.Entity(name = "ProfilingPoint")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing a profiling point. */
-public class ProfilingPointEntity implements Serializable {
+public class ProfilingPointEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(ProfilingPointEntity.class.getName());
     protected static final String DELIMITER = "#";
 
@@ -141,6 +140,12 @@
         return KeyFactory.createKey(KIND, testName + DELIMITER + profilingPointName);
     }
 
+    /** Saving function for the instance of this class */
+    @Override
+    public com.googlecode.objectify.Key<ProfilingPointEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity profilingPoint = new Entity(key);
         profilingPoint.setIndexedProperty(TEST_NAME, this.testName);
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
index 2010971..48df727 100644
--- a/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
+++ b/src/main/java/com/android/vts/entity/ProfilingPointRunEntity.java
@@ -27,22 +27,21 @@
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Parent;
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import lombok.Data;
-import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
+
+import static com.googlecode.objectify.ObjectifyService.ofy;
 
 @com.googlecode.objectify.annotation.Entity(name = "ProfilingPointRun")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing a profiling point execution. */
-public class ProfilingPointRunEntity implements Serializable {
+public class ProfilingPointRunEntity implements DashboardEntity {
     protected static final Logger logger =
             Logger.getLogger(ProfilingPointRunEntity.class.getName());
 
@@ -94,7 +93,7 @@
     /**
      * Create a ProfilingPointRunEntity object.
      *
-     * @param parentKey The Key object for the parent TestRunEntity in the database.
+     * @param parentKey The Key object for the parent TestRunEntity in datastore.
      * @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.
@@ -125,6 +124,41 @@
         this.options = options;
     }
 
+
+    /**
+     * Create a ProfilingPointRunEntity object.
+     *
+     * @param parent The objectify Key for the parent TestRunEntity in datastore.
+     * @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(
+            com.googlecode.objectify.Key parent,
+            String name,
+            int type,
+            int regressionMode,
+            List<String> labels,
+            List<Long> values,
+            String xLabel,
+            String yLabel,
+            List<String> options) {
+        this.parent = parent;
+        this.name = name;
+        this.type = type;
+        this.regressionMode = regressionMode;
+        this.labels = labels == null ? null : new ArrayList<>(labels);
+        this.values = new ArrayList<>(values);
+        this.xLabel = xLabel;
+        this.yLabel = yLabel;
+        this.options = options;
+    }
+
     /**
      * Get VtsProfilingType from int value.
      *
@@ -143,6 +177,12 @@
         return VtsProfilingRegressionMode.forNumber(regressionMode);
     }
 
+    /** Saving function for the instance of this class */
+    @Override
+    public com.googlecode.objectify.Key<ProfilingPointRunEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity profilingRun = new Entity(this.key);
         profilingRun.setUnindexedProperty(TYPE, this.type);
@@ -207,12 +247,12 @@
     /**
      * Convert a coverage report to a CoverageEntity.
      *
-     * @param parentKey The ancestor key for the coverage entity.
+     * @param parent The ancestor objectify 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) {
+            com.googlecode.objectify.Key parent, ProfilingReportMessage profilingReport) {
         if (!profilingReport.hasName()
                 || !profilingReport.hasType()
                 || profilingReport.getType() == VtsProfilingType.UNKNOWN_VTS_PROFILING_TYPE
@@ -265,7 +305,7 @@
             }
         }
         return new ProfilingPointRunEntity(
-                parentKey,
+                parent,
                 name,
                 type.getNumber(),
                 regressionMode.getNumber(),
diff --git a/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java b/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java
index f426707..e4a1911 100644
--- a/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java
+++ b/src/main/java/com/android/vts/entity/ProfilingPointSummaryEntity.java
@@ -25,7 +25,6 @@
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Index;
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -33,16 +32,16 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import lombok.Data;
-import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
+
+import static com.googlecode.objectify.ObjectifyService.ofy;
 
 @com.googlecode.objectify.annotation.Entity(name = "ProfilingPointSummary")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing a profiling point summary. */
-public class ProfilingPointSummaryEntity implements Serializable {
+public class ProfilingPointSummaryEntity implements DashboardEntity {
     protected static final Logger logger =
             Logger.getLogger(ProfilingPointSummaryEntity.class.getName());
     protected static final String DELIMITER = "#";
@@ -237,6 +236,12 @@
         }
     }
 
+    /** Saving function for the instance of this class */
+    @Override
+    public com.googlecode.objectify.Key<ProfilingPointSummaryEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity profilingSummary;
         profilingSummary = new Entity(this.key);
diff --git a/src/main/java/com/android/vts/entity/RoleEntity.java b/src/main/java/com/android/vts/entity/RoleEntity.java
index d001cfa..508a9ca 100644
--- a/src/main/java/com/android/vts/entity/RoleEntity.java
+++ b/src/main/java/com/android/vts/entity/RoleEntity.java
@@ -6,49 +6,38 @@
 import com.googlecode.objectify.annotation.Cache;
 import com.googlecode.objectify.annotation.Entity;
 import com.googlecode.objectify.annotation.Id;
-import com.googlecode.objectify.annotation.Index;
-import com.googlecode.objectify.annotation.Load;
-import java.io.Serializable;
-import java.util.ArrayList;
 import java.util.Date;
-import java.util.List;
-import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-
 @Cache
 @Entity
 @EqualsAndHashCode(of = "role")
 @NoArgsConstructor
-public class RoleEntity implements Serializable {
+public class RoleEntity implements DashboardEntity {
 
-  private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
-  @Id
-  private String role;
+    @Id private String role;
 
-  /** When this record was created or updated */
-  @Getter
-  Date updated;
+    /** When this record was created or updated */
+    @Getter Date updated;
 
-  /** Construction function for UserEntity Class */
-  public RoleEntity(String roleName) {
-    this.role = roleName;
-  }
+    /** Construction function for UserEntity Class */
+    public RoleEntity(String roleName) {
+        this.role = roleName;
+    }
 
-  /** Get role by email */
-  public static RoleEntity getRole(String role) {
-    return ofy().load()
-        .type(RoleEntity.class)
-        .id(role)
-        .now();
-  }
+    /** Get role by email */
+    public static RoleEntity getRole(String role) {
+        return ofy().load().type(RoleEntity.class).id(role).now();
+    }
 
-  /** Saving function for the instance of this class */
-  public void save() {
-    this.updated = new Date();
-    ofy().save().entity(this).now();
-  }
-}
\ No newline at end of file
+    /** Saving function for the instance of this class */
+    @Override
+    public Key<RoleEntity> save() {
+        this.updated = new Date();
+        return ofy().save().entity(this).now();
+    }
+}
diff --git a/src/main/java/com/android/vts/entity/TestAcknowledgmentEntity.java b/src/main/java/com/android/vts/entity/TestAcknowledgmentEntity.java
index 66b56b8..a594f85 100644
--- a/src/main/java/com/android/vts/entity/TestAcknowledgmentEntity.java
+++ b/src/main/java/com/android/vts/entity/TestAcknowledgmentEntity.java
@@ -26,6 +26,12 @@
 import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import com.google.gson.reflect.TypeToken;
+import com.googlecode.objectify.annotation.Cache;
+import com.googlecode.objectify.annotation.Id;
+import com.googlecode.objectify.annotation.Ignore;
+import com.googlecode.objectify.annotation.Index;
+import lombok.Data;
+
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -35,6 +41,11 @@
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import static com.googlecode.objectify.ObjectifyService.ofy;
+
+@com.googlecode.objectify.annotation.Entity(name = "TestAcknowledgment")
+@Cache
+@Data
 /** Entity describing a test failure acknowledgment. */
 public class TestAcknowledgmentEntity implements DashboardEntity {
     protected static final Logger logger =
@@ -44,21 +55,27 @@
     public static final String KEY = "key";
     public static final String TEST_KEY = "testKey";
     public static final String TEST_NAME = "testName";
-    public static final String USER = "user";
+    public static final String USER_OBJ = "userObj";
     public static final String CREATED = "created";
     public static final String BRANCHES = "branches";
     public static final String DEVICES = "devices";
     public static final String TEST_CASE_NAMES = "testCaseNames";
     public static final String NOTE = "note";
 
-    private final Key key;
-    private final long created;
-    public final Key test;
-    public final User user;
-    public final Set<String> branches;
-    public final Set<String> devices;
-    public final Set<String> testCaseNames;
-    public final String note;
+    @Ignore private final Key key;
+    @Ignore public final Key test;
+    @Ignore public final User userObj;
+
+    @Id private Long id;
+
+    private com.googlecode.objectify.Key testKey;
+    private Set<String> branches;
+    private Set<String> devices;
+    private Set<String> testCaseNames;
+    private String note;
+    private String user;
+
+    @Index private final long created;
 
     /**
      * Create a AcknowledgmentEntity object.
@@ -66,11 +83,11 @@
      * @param key The key of the AcknowledgmentEntity in the database.
      * @param created The timestamp when the entity was created (in microseconds).
      * @param test The key of the test.
-     * @param user The user who created or last modified the entity.
+     * @param userObj The user who created or last modified the entity.
      * @param branches The list of branch names for which the acknowledgment applies (or null if
      *     all).
-     * @param devices The list of device build flavors for which the acknowledgment applies (or
-     *     null if all).
+     * @param devices The list of device build flavors for which the acknowledgment applies (or null
+     *     if all).
      * @param testCaseNames The list of test case names known to fail (or null if all).
      * @param note A text blob with details about the failure (or null if all).
      */
@@ -78,13 +95,13 @@
             Key key,
             long created,
             Key test,
-            User user,
+            User userObj,
             List<String> branches,
             List<String> devices,
             List<String> testCaseNames,
             Text note) {
         this.test = test;
-        this.user = user;
+        this.userObj = userObj;
         if (branches != null) this.branches = new HashSet(branches);
         else this.branches = new HashSet<>();
 
@@ -105,32 +122,37 @@
      * Create a AcknowledgmentEntity object.
      *
      * @param test The key of the test.
-     * @param user The user who created or last modified the entity.
+     * @param userObj The user who created or last modified the entity.
      * @param branches The list of branch names for which the acknowledgment applies (or null if
      *     all).
-     * @param devices The list of device build flavors for which the acknowledgment applies (or
-     *     null if all).
+     * @param devices The list of device build flavors for which the acknowledgment applies (or null
+     *     if all).
      * @param testCaseNames The list of test case names known to fail (or null if all).
      * @param note A text blob with details about the failure (or null if all).
      */
     public TestAcknowledgmentEntity(
             Key test,
-            User user,
+            User userObj,
             List<String> branches,
             List<String> devices,
             List<String> testCaseNames,
             Text note) {
-        this(null, -1, test, user, branches, devices, testCaseNames, note);
+        this(null, -1, test, userObj, branches, devices, testCaseNames, note);
     }
 
+    /** Saving function for the instance of this class */
     @Override
+    public com.googlecode.objectify.Key<TestAcknowledgmentEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity ackEntity;
         if (this.key == null) ackEntity = new Entity(KIND);
         else ackEntity = new Entity(key);
 
         ackEntity.setProperty(TEST_KEY, this.test);
-        ackEntity.setProperty(USER, this.user);
+        ackEntity.setProperty(USER_OBJ, this.userObj);
 
         long created = this.created;
         if (created < 0) created = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
@@ -159,7 +181,7 @@
     public static TestAcknowledgmentEntity fromEntity(Entity e) {
         if (!e.getKind().equals(KIND)
                 || !e.hasProperty(TEST_KEY)
-                || !e.hasProperty(USER)
+                || !e.hasProperty(USER_OBJ)
                 || !e.hasProperty(CREATED)) {
             logger.log(
                     Level.WARNING, "Missing attributes in acknowledgment entity: " + e.toString());
@@ -167,7 +189,7 @@
         }
         try {
             Key test = (Key) e.getProperty(TEST_KEY);
-            User user = (User) e.getProperty(USER);
+            User user = (User) e.getProperty(USER_OBJ);
             long created = (long) e.getProperty(CREATED);
 
             List<String> branches;
@@ -247,7 +269,7 @@
         JsonObject json = new JsonObject();
         json.add(KEY, new JsonPrimitive(KeyFactory.keyToString(this.key)));
         json.add(TEST_NAME, new JsonPrimitive(this.test.getName()));
-        json.add(USER, new JsonPrimitive(this.user.getEmail()));
+        json.add(USER_OBJ, new JsonPrimitive(this.userObj.getEmail()));
         json.add(CREATED, new JsonPrimitive(this.created));
 
         List<JsonElement> branches = new ArrayList<>();
diff --git a/src/main/java/com/android/vts/entity/TestCaseRunEntity.java b/src/main/java/com/android/vts/entity/TestCaseRunEntity.java
index 6d3ba83..30776d4 100644
--- a/src/main/java/com/android/vts/entity/TestCaseRunEntity.java
+++ b/src/main/java/com/android/vts/entity/TestCaseRunEntity.java
@@ -24,17 +24,17 @@
 import com.googlecode.objectify.annotation.OnLoad;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.googlecode.objectify.ObjectifyService.ofy;
 
 @com.googlecode.objectify.annotation.Entity(name = "TestCaseRun")
 @Cache
 @Data
+@Slf4j
 /** 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";
 
@@ -89,7 +89,8 @@
      * Create a TestCaseRunEntity.
      */
     public TestCaseRunEntity() {
-        this.id = -1L;
+        this.results = new ArrayList<>();
+        this.testCaseNames = new ArrayList<>();
         this.testCases = new ArrayList<>();
         this.systraceUrl = null;
     }
@@ -100,6 +101,8 @@
      */
     public TestCaseRunEntity(long id) {
         this.id = id;
+        this.results = new ArrayList<>();
+        this.testCaseNames = new ArrayList<>();
         this.testCases = new ArrayList<>();
         this.systraceUrl = null;
     }
@@ -147,13 +150,21 @@
      * @return true if added, false otherwise.
      */
     public boolean addTestCase(String name, int result) {
-        if (isFull())
+        if (this.isFull()) {
             return false;
-        this.testCases.add(new TestCase(this.id, this.testCases.size(), name, result));
-        return true;
+        } else {
+            this.testCaseNames.add(name);
+            this.results.add(result);
+            return true;
+        }
     }
 
+    /** Saving function for the instance of this class */
     @Override
+    public com.googlecode.objectify.Key<TestCaseRunEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity testCaseRunEntity;
         if (this.id >= 0) {
@@ -188,7 +199,7 @@
     @SuppressWarnings("unchecked")
     public static TestCaseRunEntity fromEntity(Entity e) {
         if (!e.getKind().equals(KIND)) {
-            logger.log(Level.WARNING, "Wrong kind: " + e.getKey());
+            log.warn("Wrong kind: " + e.getKey());
             return null;
         }
         try {
@@ -213,7 +224,7 @@
             return testCaseRun;
         } catch (ClassCastException exception) {
             // Invalid cast
-            logger.log(Level.WARNING, "Error parsing test case run entity.", exception);
+            log.warn("Error parsing test case run entity.", exception);
         }
         return null;
     }
diff --git a/src/main/java/com/android/vts/entity/TestCoverageStatusEntity.java b/src/main/java/com/android/vts/entity/TestCoverageStatusEntity.java
index cc630c3..1156cc7 100644
--- a/src/main/java/com/android/vts/entity/TestCoverageStatusEntity.java
+++ b/src/main/java/com/android/vts/entity/TestCoverageStatusEntity.java
@@ -23,7 +23,6 @@
 import com.googlecode.objectify.annotation.Cache;
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Index;
-import java.io.Serializable;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -42,7 +41,7 @@
 @Cache
 @NoArgsConstructor
 /** Entity describing test coverage status. */
-public class TestCoverageStatusEntity implements Serializable {
+public class TestCoverageStatusEntity implements DashboardEntity {
 
     protected static final Logger logger =
             Logger.getLogger(TestCoverageStatusEntity.class.getName());
@@ -190,9 +189,10 @@
     }
 
     /** Saving function for the instance of this class */
-    public void save() {
+    @Override
+    public Key<TestCoverageStatusEntity> save() {
         this.updatedDate = new Date();
-        ofy().save().entity(this).now();
+        return ofy().save().entity(this).now();
     }
 
     public Entity toEntity() {
diff --git a/src/main/java/com/android/vts/entity/TestEntity.java b/src/main/java/com/android/vts/entity/TestEntity.java
index e48d759..4df7fb0 100644
--- a/src/main/java/com/android/vts/entity/TestEntity.java
+++ b/src/main/java/com/android/vts/entity/TestEntity.java
@@ -24,7 +24,6 @@
 import com.googlecode.objectify.annotation.Entity;
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Index;
-import java.io.Serializable;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -34,12 +33,12 @@
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 
-@Entity(name="Test")
+@Entity(name = "Test")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing test metadata. */
-public class TestEntity implements Serializable {
+public class TestEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(TestEntity.class.getName());
 
     public static final String KIND = "Test";
@@ -75,6 +74,12 @@
         this(testName, false);
     }
 
+    /** Saving function for the instance of this class */
+    @Override
+    public com.googlecode.objectify.Key<TestEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public com.google.appengine.api.datastore.Entity toEntity() {
         com.google.appengine.api.datastore.Entity testEntity = new com.google.appengine.api.datastore.Entity(this.getOldKey());
         testEntity.setProperty(HAS_PROFILING_DATA, this.hasProfilingData);
@@ -82,6 +87,19 @@
     }
 
     /**
+     * Get objectify TestRun Entity's key.
+     *
+     * @param startTimestamp test start timestamp
+     */
+    public com.googlecode.objectify.Key getTestRunKey(long startTimestamp) {
+        com.googlecode.objectify.Key testKey =
+                com.googlecode.objectify.Key.create(TestEntity.class, this.getTestName());
+        com.googlecode.objectify.Key testRunKey =
+                com.googlecode.objectify.Key.create(testKey, TestRunEntity.class, startTimestamp);
+        return testRunKey;
+    }
+
+    /**
      * Get key info from appengine based library.
      */
     public Key getOldKey() {
@@ -130,10 +148,4 @@
         }
         return new TestEntity(testName, hasProfilingData);
     }
-
-    /** Saving function for the instance of this class */
-    public void save() {
-        ofy().save().entity(this).now();
-    }
-
 }
diff --git a/src/main/java/com/android/vts/entity/TestPlanEntity.java b/src/main/java/com/android/vts/entity/TestPlanEntity.java
index 805c3cd..590db46 100644
--- a/src/main/java/com/android/vts/entity/TestPlanEntity.java
+++ b/src/main/java/com/android/vts/entity/TestPlanEntity.java
@@ -19,21 +19,20 @@
 import static com.googlecode.objectify.ObjectifyService.ofy;
 
 import com.google.appengine.api.datastore.Entity;
+import com.googlecode.objectify.Key;
 import com.googlecode.objectify.annotation.Cache;
 import com.googlecode.objectify.annotation.Id;
-import java.io.Serializable;
-import java.util.Date;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-@com.googlecode.objectify.annotation.Entity(name="TestPlan")
+@com.googlecode.objectify.annotation.Entity(name = "TestPlan")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing test plan metadata. */
-public class TestPlanEntity implements Serializable {
+public class TestPlanEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(TestPlanEntity.class.getName());
 
     public static final String KIND = "TestPlan";
@@ -58,6 +57,11 @@
         return planEntity;
     }
 
+    public Key getKey() {
+        Key key = Key.create(TestPlanEntity.class, this.testPlanName);
+        return key;
+    }
+
     /**
      * Convert an Entity object to a TestEntity.
      *
@@ -76,7 +80,8 @@
     }
 
     /** Saving function for the instance of this class */
-    public void save() {
-        ofy().save().entity(this).now();
+    @Override
+    public com.googlecode.objectify.Key<TestPlanEntity> save() {
+        return ofy().save().entity(this).now();
     }
 }
diff --git a/src/main/java/com/android/vts/entity/TestPlanRunEntity.java b/src/main/java/com/android/vts/entity/TestPlanRunEntity.java
index 7dbb0dc..de2e505 100644
--- a/src/main/java/com/android/vts/entity/TestPlanRunEntity.java
+++ b/src/main/java/com/android/vts/entity/TestPlanRunEntity.java
@@ -18,7 +18,6 @@
 
 import static com.googlecode.objectify.ObjectifyService.ofy;
 
-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;
@@ -32,25 +31,21 @@
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Index;
 import com.googlecode.objectify.annotation.Parent;
-import java.io.Serializable;
 import java.util.Date;
 import java.util.List;
 import java.util.Objects;
-import java.util.UUID;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import lombok.Data;
-import lombok.Getter;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
 
 @com.googlecode.objectify.annotation.Entity(name = "TestPlanRun")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing test plan run information. */
-public class TestPlanRunEntity implements Serializable {
+public class TestPlanRunEntity implements DashboardEntity {
 
     protected static final Logger logger = Logger.getLogger(TestPlanRunEntity.class.getName());
 
@@ -76,7 +71,7 @@
 
     @Id private Long id;
 
-    @Parent private com.googlecode.objectify.Key<TestPlanEntity> testParent;
+    @Parent private com.googlecode.objectify.Key<TestPlanEntity> parent;
 
     @Index private String testPlanName;
 
@@ -98,7 +93,7 @@
 
     @Ignore private List<Key> oldTestRuns;
 
-    private List<com.googlecode.objectify.Key<?>> testRuns;
+    private List<com.googlecode.objectify.Key<TestRunEntity>> testRuns;
 
     /** When this record was created or updated */
     @Index Date updated;
@@ -106,7 +101,7 @@
     /**
      * Create a TestPlanRunEntity object describing a test plan run.
      *
-     * @param parentKey The key for the parent entity in the database.
+     * @param testPlanKey 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.
@@ -116,7 +111,7 @@
      * @param testRuns A list of keys to the TestRunEntity objects for the plan run run.
      */
     public TestPlanRunEntity(
-            Key parentKey,
+            Key testPlanKey,
             String testPlanName,
             long type,
             long startTimestamp,
@@ -127,7 +122,8 @@
             long totalApiCount,
             long coveredApiCount,
             List<Key> testRuns) {
-        this.key = KeyFactory.createKey(parentKey, KIND, startTimestamp);
+        this.id = startTimestamp;
+        this.key = KeyFactory.createKey(testPlanKey, KIND, startTimestamp);
         this.testPlanName = testPlanName;
         this.type = type;
         this.startTimestamp = startTimestamp;
@@ -152,6 +148,44 @@
                         .collect(Collectors.toList());
     }
 
+    /**
+     * Create a TestPlanRunEntity object describing a test plan run.
+     *
+     * @param testPlanKey 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(
+            com.googlecode.objectify.Key<TestPlanEntity> testPlanKey,
+            String testPlanName,
+            long type,
+            long startTimestamp,
+            long endTimestamp,
+            String testBuildId,
+            long passCount,
+            long failCount,
+            long totalApiCount,
+            long coveredApiCount,
+            List<com.googlecode.objectify.Key<TestRunEntity>> testRuns) {
+        this.id = startTimestamp;
+        this.parent = testPlanKey;
+        this.testPlanName = testPlanName;
+        this.type = type;
+        this.startTimestamp = startTimestamp;
+        this.endTimestamp = endTimestamp;
+        this.testBuildId = testBuildId;
+        this.passCount = passCount;
+        this.failCount = failCount;
+        this.totalApiCount = totalApiCount;
+        this.coveredApiCount = coveredApiCount;
+        this.testRuns = testRuns;
+    }
+
     public Entity toEntity() {
         Entity planRun = new Entity(this.key);
         planRun.setProperty(TEST_PLAN_NAME, this.testPlanName);
@@ -168,6 +202,7 @@
     }
 
     /** Saving function for the instance of this class */
+    @Override
     public com.googlecode.objectify.Key<TestPlanRunEntity> save() {
         this.updated = new Date();
         return ofy().save().entity(this).now();
@@ -175,12 +210,7 @@
 
     /** Get UrlSafeKey from this class */
     public String getUrlSafeKey() {
-        com.googlecode.objectify.Key testPlanKey =
-                com.googlecode.objectify.Key.create(TestPlanEntity.class, this.testPlanName);
-        com.googlecode.objectify.Key idKey =
-                com.googlecode.objectify.Key.create(
-                        testPlanKey, TestPlanRunEntity.class, this.startTimestamp);
-        return idKey.toUrlSafe();
+        return this.getOfyKey().toUrlSafe();
     }
 
     /** Add a task to calculate the total number of coverage API */
@@ -191,7 +221,7 @@
             Queue queue = QueueFactory.getQueue(QUEUE_NAME);
             queue.add(
                     TaskOptions.Builder.withUrl(COVERAGE_API_URL)
-                            .param("urlSafeKey", String.valueOf(this.getUrlSafeKey()))
+                            .param("urlSafeKey", this.getUrlSafeKey())
                             .method(TaskOptions.Method.POST));
         }
     }
@@ -205,6 +235,12 @@
         return KeyFactory.createKey(parentKey, KIND, startTimestamp);
     }
 
+    /** Get key info from objecitfy library. */
+    public com.googlecode.objectify.Key getOfyKey() {
+        return com.googlecode.objectify.Key.create(
+                this.parent, TestPlanRunEntity.class, this.startTimestamp);
+    }
+
     /**
      * Convert an Entity object to a TestPlanRunEntity.
      *
diff --git a/src/main/java/com/android/vts/entity/TestRunEntity.java b/src/main/java/com/android/vts/entity/TestRunEntity.java
index bdd0ac8..0d5f1ec 100644
--- a/src/main/java/com/android/vts/entity/TestRunEntity.java
+++ b/src/main/java/com/android/vts/entity/TestRunEntity.java
@@ -35,9 +35,7 @@
 import com.googlecode.objectify.annotation.Index;
 import com.googlecode.objectify.annotation.OnLoad;
 import com.googlecode.objectify.annotation.Parent;
-import java.io.Serializable;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -45,20 +43,17 @@
 import java.util.function.Supplier;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import lombok.Data;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 import org.apache.commons.lang3.math.NumberUtils;
-import org.json.JSONArray;
 
 @com.googlecode.objectify.annotation.Entity(name = "TestRun")
 @Cache
 @NoArgsConstructor
 /** Entity describing test run information. */
-public class TestRunEntity implements Serializable {
+public class TestRunEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(TestRunEntity.class.getName());
 
     /** Enum for classifying test run types. */
@@ -168,7 +163,7 @@
 
     @Index @Getter @Setter private boolean hasCodeCoverage;
 
-    private com.googlecode.objectify.Key<CodeCoverageEntity> codeCoverageEntityKey;
+    @Ignore private com.googlecode.objectify.Key<CodeCoverageEntity> codeCoverageEntityKey;
 
     @Index @Getter @Setter private long coveredLineCount;
 
@@ -202,6 +197,7 @@
             boolean hasCodeCoverage,
             List<Long> testCaseIds,
             List<String> logLinks) {
+        this.id = startTimestamp;
         this.key = KeyFactory.createKey(parentKey, KIND, startTimestamp);
         this.type = type;
         this.startTimestamp = startTimestamp;
@@ -212,9 +208,11 @@
         this.failCount = failCount;
         this.hasCodeCoverage = hasCodeCoverage;
         this.testName = parentKey.getName();
-        this.codeCoverageEntityKey = getCodeCoverageEntityKey();
         this.testCaseIds = testCaseIds;
         this.logLinks = logLinks;
+
+        this.testRunParent = com.googlecode.objectify.Key.create(TestEntity.class, testName);
+        this.codeCoverageEntityKey = getCodeCoverageEntityKey();
     }
 
     /**
@@ -247,6 +245,7 @@
     }
 
     /** Saving function for the instance of this class */
+    @Override
     public com.googlecode.objectify.Key<TestRunEntity> save() {
         return ofy().save().entity(this).now();
     }
@@ -289,7 +288,7 @@
     }
 
     /** Get ApiCoverageEntity Key from the parent key */
-    private com.googlecode.objectify.Key getOfyKey() {
+    public com.googlecode.objectify.Key getOfyKey() {
         com.googlecode.objectify.Key testKey =
                 com.googlecode.objectify.Key.create(
                         TestEntity.class, this.testName);
diff --git a/src/main/java/com/android/vts/entity/TestStatusEntity.java b/src/main/java/com/android/vts/entity/TestStatusEntity.java
index ec5ee36..b703c64 100644
--- a/src/main/java/com/android/vts/entity/TestStatusEntity.java
+++ b/src/main/java/com/android/vts/entity/TestStatusEntity.java
@@ -22,7 +22,6 @@
 import com.googlecode.objectify.annotation.Id;
 import com.googlecode.objectify.annotation.Ignore;
 import com.googlecode.objectify.annotation.Index;
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
@@ -30,12 +29,14 @@
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
+import static com.googlecode.objectify.ObjectifyService.ofy;
+
 @com.googlecode.objectify.annotation.Entity(name = "TestStatus")
 @Cache
 @Data
 @NoArgsConstructor
 /** Entity describing test status. */
-public class TestStatusEntity implements Serializable {
+public class TestStatusEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(TestStatusEntity.class.getName());
 
     public static final String KIND = "TestStatus";
@@ -125,6 +126,12 @@
         this(testName, 0, -1, -1, new ArrayList<TestCaseReference>());
     }
 
+    /** Saving function for the instance of this class */
+    @Override
+    public com.googlecode.objectify.Key<TestStatusEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity testEntity = new Entity(KIND, this.testName);
         if (this.updatedTimestamp >= 0 && this.passCount >= 0 && this.failCount >= 0) {
diff --git a/src/main/java/com/android/vts/entity/TestSuiteFileEntity.java b/src/main/java/com/android/vts/entity/TestSuiteFileEntity.java
index 2331e42..c68f2ec 100644
--- a/src/main/java/com/android/vts/entity/TestSuiteFileEntity.java
+++ b/src/main/java/com/android/vts/entity/TestSuiteFileEntity.java
@@ -16,6 +16,7 @@
 
 package com.android.vts.entity;
 
+import com.googlecode.objectify.Key;
 import com.googlecode.objectify.annotation.Cache;
 import com.googlecode.objectify.annotation.Entity;
 import com.googlecode.objectify.annotation.Id;
@@ -25,11 +26,9 @@
 import lombok.NoArgsConstructor;
 import lombok.Setter;
 
-import java.nio.file.FileSystems;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Date;
-import java.util.List;
 
 import static com.googlecode.objectify.ObjectifyService.ofy;
 
@@ -38,7 +37,7 @@
 @Entity
 @EqualsAndHashCode(of = "id")
 @NoArgsConstructor
-public class TestSuiteFileEntity {
+public class TestSuiteFileEntity implements DashboardEntity {
 
     /** Test Suite full file path field */
     @Id @Getter @Setter String filePath;
@@ -71,8 +70,9 @@
     }
 
     /** Saving function for the instance of this class */
-    public void save() {
+    @Override
+    public Key<TestSuiteFileEntity> save() {
         this.updated = new Date();
-        ofy().save().entity(this).now();
+        return ofy().save().entity(this).now();
     }
 }
diff --git a/src/main/java/com/android/vts/entity/TestSuiteResultEntity.java b/src/main/java/com/android/vts/entity/TestSuiteResultEntity.java
index 2e12f54..95ba5c8 100644
--- a/src/main/java/com/android/vts/entity/TestSuiteResultEntity.java
+++ b/src/main/java/com/android/vts/entity/TestSuiteResultEntity.java
@@ -38,18 +38,13 @@
 import org.apache.http.client.utils.URLEncodedUtils;
 import org.apache.http.message.BasicNameValuePair;
 
-import javax.servlet.ServletContext;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
-import java.math.RoundingMode;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
@@ -126,7 +121,7 @@
 @Entity
 @EqualsAndHashCode(of = "id")
 @NoArgsConstructor
-public class TestSuiteResultEntity {
+public class TestSuiteResultEntity implements DashboardEntity {
 
     private static final Logger logger = Logger.getLogger(TestSuiteResultEntity.class.getName());
 
@@ -316,6 +311,12 @@
         }
     }
 
+    /** Saving function for the instance of this class */
+    @Override
+    public Key<TestSuiteResultEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public static void setPropertyValues(Properties newSystemConfigProp) {
         systemConfigProp = newSystemConfigProp;
         bugTrackingSystemProp = getBugTrackingSystemProp(newSystemConfigProp);
diff --git a/src/main/java/com/android/vts/entity/UserEntity.java b/src/main/java/com/android/vts/entity/UserEntity.java
index a686efb..43b5edd 100644
--- a/src/main/java/com/android/vts/entity/UserEntity.java
+++ b/src/main/java/com/android/vts/entity/UserEntity.java
@@ -38,7 +38,7 @@
 @Entity
 @EqualsAndHashCode(of = "email")
 @NoArgsConstructor
-public class UserEntity {
+public class UserEntity implements DashboardEntity {
 
     /** User email field */
     @Id @Getter @Setter String email;
@@ -79,9 +79,10 @@
     }
 
     /** Saving function for the instance of this class */
-    public void save() {
+    @Override
+    public Key<UserEntity> save() {
         this.updated = new Date();
-        ofy().save().entity(this).now();
+        return ofy().save().entity(this).now();
     }
 
     /** Get admin user list by admin email */
diff --git a/src/main/java/com/android/vts/entity/UserFavoriteEntity.java b/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
index 132d9b4..2548042 100644
--- a/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
+++ b/src/main/java/com/android/vts/entity/UserFavoriteEntity.java
@@ -19,9 +19,22 @@
 import com.google.appengine.api.datastore.Entity;
 import com.google.appengine.api.datastore.Key;
 import com.google.appengine.api.users.User;
+import com.googlecode.objectify.annotation.Cache;
+import com.googlecode.objectify.annotation.Ignore;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import static com.googlecode.objectify.ObjectifyService.ofy;
+
+@Cache
+@com.googlecode.objectify.annotation.Entity(name = "UserFavorite")
+@EqualsAndHashCode(of = "email")
+@NoArgsConstructor
 /** Entity describing subscriptions between a user and a test. */
 public class UserFavoriteEntity implements DashboardEntity {
     protected static final Logger logger = Logger.getLogger(UserFavoriteEntity.class.getName());
@@ -33,10 +46,17 @@
     public static final String TEST_KEY = "testKey";
     public static final String MUTE_NOTIFICATIONS = "muteNotifications";
 
-    private final Key key;
-    public final User user;
-    public final Key testKey;
-    public boolean muteNotifications;
+    @Ignore private Key key = null;
+
+    @Ignore public User user = null;
+
+    @Ignore public Key testKey = null;
+
+    @Getter @Setter private com.googlecode.objectify.Key<TestEntity> test;
+
+    @Getter @Setter private String userEmail;
+
+    @Getter @Setter public boolean muteNotifications;
 
     /**
      * Create a user favorite relationship.
@@ -64,7 +84,12 @@
         this(null, user, testKey, muteNotifications);
     }
 
+    /** Saving function for the instance of this class */
     @Override
+    public com.googlecode.objectify.Key<UserFavoriteEntity> save() {
+        return ofy().save().entity(this).now();
+    }
+
     public Entity toEntity() {
         Entity favoriteEntity;
         if (this.key != null) {
diff --git a/src/main/java/com/android/vts/job/BaseJobServlet.java b/src/main/java/com/android/vts/job/BaseJobServlet.java
index 3d6232b..18a1d24 100644
--- a/src/main/java/com/android/vts/job/BaseJobServlet.java
+++ b/src/main/java/com/android/vts/job/BaseJobServlet.java
@@ -16,15 +16,10 @@
 
 package com.android.vts.job;
 
-import com.android.vts.servlet.BaseServlet;
-import com.android.vts.util.EmailHelper;
 
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.Properties;
 
 /**
@@ -37,20 +32,20 @@
      */
     protected static Properties systemConfigProp = new Properties();
 
+    /**
+     * This variable is for maximum number of entities per transaction You can find the detail here
+     * (https://cloud.google.com/datastore/docs/concepts/limits)
+     */
+    protected int MAX_ENTITY_SIZE_PER_TRANSACTION = 300;
+
     @Override
     public void init(ServletConfig cfg) throws ServletException {
         super.init(cfg);
 
-        try {
-            InputStream defaultInputStream =
-                    BaseServlet.class.getClassLoader().getResourceAsStream("config.properties");
-            systemConfigProp.load(defaultInputStream);
+        systemConfigProp =
+                Properties.class.cast(cfg.getServletContext().getAttribute("systemConfigProp"));
 
-            EmailHelper.setPropertyValues(systemConfigProp);
-        } catch (FileNotFoundException e) {
-            e.printStackTrace();
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
+        this.MAX_ENTITY_SIZE_PER_TRANSACTION =
+                Integer.parseInt(systemConfigProp.getProperty("datastore.maxEntitySize"));
     }
 }
diff --git a/src/main/java/com/android/vts/job/VtsAlertJobServlet.java b/src/main/java/com/android/vts/job/VtsAlertJobServlet.java
index 3336c85..be23982 100644
--- a/src/main/java/com/android/vts/job/VtsAlertJobServlet.java
+++ b/src/main/java/com/android/vts/job/VtsAlertJobServlet.java
@@ -179,29 +179,30 @@
             List<TestAcknowledgmentEntity> acks) {
         Set<String> acknowledged = new HashSet<>();
         for (TestAcknowledgmentEntity ack : acks) {
-            boolean allDevices = ack.devices == null || ack.devices.size() == 0;
-            boolean allBranches = ack.branches == null || ack.branches.size() == 0;
+            boolean allDevices = ack.getDevices() == null || ack.getDevices().size() == 0;
+            boolean allBranches = ack.getBranches() == null || ack.getBranches().size() == 0;
             boolean isRelevant = allDevices && allBranches;
 
             // Determine if the acknowledgment is relevant to the devices.
             if (!isRelevant) {
                 for (DeviceInfoEntity device : devices) {
                     boolean deviceAcknowledged =
-                            allDevices || ack.devices.contains(device.getBuildFlavor());
+                            allDevices || ack.getDevices().contains(device.getBuildFlavor());
                     boolean branchAcknowledged =
-                            allBranches || ack.branches.contains(device.getBranch());
+                            allBranches || ack.getBranches().contains(device.getBranch());
                     if (deviceAcknowledged && branchAcknowledged) isRelevant = true;
                 }
             }
 
             if (isRelevant) {
                 // Separate the test cases
-                boolean allTestCases = ack.testCaseNames == null || ack.testCaseNames.size() == 0;
+                boolean allTestCases =
+                        ack.getTestCaseNames() == null || ack.getTestCaseNames().size() == 0;
                 if (allTestCases) {
                     acknowledged.addAll(testCases);
                     testCases.removeAll(acknowledged);
                 } else {
-                    for (String testCase : ack.testCaseNames) {
+                    for (String testCase : ack.getTestCaseNames()) {
                         if (testCases.contains(testCase)) {
                             acknowledged.add(testCase);
                             testCases.remove(testCase);
diff --git a/src/main/java/com/android/vts/job/VtsSpreadSheetSyncServlet.java b/src/main/java/com/android/vts/job/VtsSpreadSheetSyncServlet.java
index 0f1bfbc..45726d9 100644
--- a/src/main/java/com/android/vts/job/VtsSpreadSheetSyncServlet.java
+++ b/src/main/java/com/android/vts/job/VtsSpreadSheetSyncServlet.java
@@ -17,6 +17,7 @@
 package com.android.vts.job;
 
 import com.android.vts.entity.ApiCoverageExcludedEntity;
+import com.android.vts.entity.DashboardEntity;
 import com.google.api.client.auth.oauth2.Credential;
 import com.google.api.client.extensions.appengine.datastore.AppEngineDataStoreFactory;
 import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
@@ -162,7 +163,7 @@
                 }
             }
 
-            ApiCoverageExcludedEntity.saveAll(apiCoverageExcludedEntities);
+            DashboardEntity.saveAll(apiCoverageExcludedEntities, MAX_ENTITY_SIZE_PER_TRANSACTION);
 
         } catch (GeneralSecurityException gse) {
             logger.log(Level.SEVERE, gse.getMessage());
diff --git a/src/main/java/com/android/vts/servlet/BaseServlet.java b/src/main/java/com/android/vts/servlet/BaseServlet.java
index 96ba561..5319ee1 100644
--- a/src/main/java/com/android/vts/servlet/BaseServlet.java
+++ b/src/main/java/com/android/vts/servlet/BaseServlet.java
@@ -26,9 +26,7 @@
 import com.google.appengine.api.users.UserServiceFactory;
 import com.google.gson.Gson;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -45,200 +43,191 @@
 
 public abstract class BaseServlet extends HttpServlet {
 
-  protected final Logger logger = Logger.getLogger(getClass().getName());
+    protected final Logger logger = Logger.getLogger(getClass().getName());
 
-  protected String ERROR_MESSAGE_JSP = "WEB-INF/jsp/error_msg.jsp";
+    protected String ERROR_MESSAGE_JSP = "WEB-INF/jsp/error_msg.jsp";
 
-  // Environment variables
-  protected static String GERRIT_URI;
-  protected static String GERRIT_SCOPE;
-  protected static String CLIENT_ID;
-  protected static String ANALYTICS_ID;
+    // Environment variables
+    protected static String GERRIT_URI;
+    protected static String GERRIT_SCOPE;
+    protected static String CLIENT_ID;
+    protected static String ANALYTICS_ID;
 
-  protected static final String TREE_DEFAULT_PARAM = "treeDefault";
+    protected static final String TREE_DEFAULT_PARAM = "treeDefault";
 
-  public enum PageType {
-    TOT("Test", "/"),
-    RELEASE("Release", "/show_release"),
-    COVERAGE_OVERVIEW("Coverage", "/show_coverage_overview"),
-    PROFILING_LIST("Profiling", "/show_profiling_list"),
-    TABLE("", "/show_table"),
-    TREE("", "/show_tree"),
-    GRAPH("Profiling", "/show_graph"),
-    COVERAGE("Coverage", "/show_coverage"),
-    PERFORMANCE_DIGEST("Performance Digest", "/show_performance_digest"),
-    PLAN_RELEASE("", "/show_plan_release"),
-    PLAN_RUN("Plan Run", "/show_plan_run"),
-    PROFILING_OVERVIEW("", "/show_profiling_overview");
+    public enum PageType {
+        TOT("Test", "/"),
+        RELEASE("Release", "/show_release"),
+        COVERAGE_OVERVIEW("Coverage", "/show_coverage_overview"),
+        PROFILING_LIST("Profiling", "/show_profiling_list"),
+        TABLE("", "/show_table"),
+        TREE("", "/show_tree"),
+        GRAPH("Profiling", "/show_graph"),
+        COVERAGE("Coverage", "/show_coverage"),
+        PERFORMANCE_DIGEST("Performance Digest", "/show_performance_digest"),
+        PLAN_RELEASE("", "/show_plan_release"),
+        PLAN_RUN("Plan Run", "/show_plan_run"),
+        PROFILING_OVERVIEW("", "/show_profiling_overview");
 
-    public final String defaultName;
-    public final String defaultUrl;
+        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 name, String url, Boolean withoutDefault) {
-      this.type = type;
-      this.name = 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));
-    links.add(new Page(PageType.PROFILING_LIST));
-    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);
-
-  /**
-   * System Configuration Property class
-   */
-  protected static Properties systemConfigProp = new Properties();
-
-  @Override
-  public void init(ServletConfig cfg) throws ServletException {
-    super.init(cfg);
-
-    try {
-      InputStream defaultInputStream =
-          BaseServlet.class.getClassLoader().getResourceAsStream("config.properties");
-      systemConfigProp.load(defaultInputStream);
-
-      GERRIT_URI = systemConfigProp.getProperty("gerrit.uri");
-      GERRIT_SCOPE = systemConfigProp.getProperty("gerrit.scope");
-      CLIENT_ID = systemConfigProp.getProperty("appengine.clientID");
-      ANALYTICS_ID = systemConfigProp.getProperty("analytics.id");
-
-      CoverageEntity.setPropertyValues(systemConfigProp);
-      TestSuiteResultEntity.setPropertyValues(systemConfigProp);
-      EmailHelper.setPropertyValues(systemConfigProp);
-      GcsHelper.setGcsProjectId(systemConfigProp.getProperty("gcs.projectID"));
-    } catch (FileNotFoundException e) {
-      e.printStackTrace();
-    } catch (IOException e) {
-      e.printStackTrace();
-    }
-  }
-
-  @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();
-    Optional<User> currentUser = Optional.ofNullable(userService.getCurrentUser());
-    String currentUserEmail =
-        currentUser.isPresent()
-            ? currentUser.map(user -> user.getEmail().trim()).orElse("")
-            : "";
-    String requestUri = request.getRequestURI();
-    String requestArgs = request.getQueryString();
-    String loginURI = userService.createLoginURL(requestUri + '?' + requestArgs);
-    String logoutURI = userService.createLogoutURL(loginURI);
-    if (currentUserEmail != "") {
-
-      int activeIndex;
-      switch (getNavParentType()) {
-        case PROFILING_LIST:
-          activeIndex = 3;
-          break;
-        case COVERAGE_OVERVIEW:
-          activeIndex = 2;
-          break;
-        case RELEASE:
-          activeIndex = 1;
-          break;
-        default:
-          activeIndex = 0;
-          break;
-      }
-      if (request.getParameter(TREE_DEFAULT_PARAM) != null) {
-        HttpSession session = request.getSession(true);
-        boolean treeDefault = request.getParameter(TREE_DEFAULT_PARAM).equals("true");
-        session.setAttribute(TREE_DEFAULT_PARAM, treeDefault);
-      }
-
-      request.setAttribute("serverName", request.getServerName());
-      request.setAttribute("logoutURL", logoutURI);
-      request.setAttribute("email", currentUserEmail);
-      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");
-
-      if (currentUserEmail.endsWith("@google.com") || UserEntity.getUserList()
-          .contains(currentUserEmail)) {
-        doGetHandler(request, response);
-      } else {
-        RequestDispatcher dispatcher =
-            request.getRequestDispatcher("WEB-INF/jsp/auth_error.jsp");
-        try {
-          dispatcher.forward(request, response);
-        } catch (ServletException e) {
-          logger.log(Level.SEVERE, "Servlet Exception caught : ", e);
+        PageType(String defaultName, String defaultUrl) {
+            this.defaultName = defaultName;
+            this.defaultUrl = defaultUrl;
         }
-      }
-    } else {
-      response.sendRedirect(loginURI);
     }
-  }
 
-  /**
-   * Implementation of the doGet method to be executed by servlet subclasses.
-   *
-   * @param request The HttpServletRequest object.
-   * @param response The HttpServletResponse object.
-   */
-  public abstract void doGetHandler(HttpServletRequest request, HttpServletResponse response)
-      throws IOException;
+    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 name, String url, Boolean withoutDefault) {
+            this.type = type;
+            this.name = 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));
+        links.add(new Page(PageType.PROFILING_LIST));
+        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);
+
+    /** System Configuration Property class */
+    protected static Properties systemConfigProp = new Properties();
+
+    @Override
+    public void init(ServletConfig cfg) throws ServletException {
+        super.init(cfg);
+
+        systemConfigProp =
+                Properties.class.cast(cfg.getServletContext().getAttribute("systemConfigProp"));
+
+        GERRIT_URI = systemConfigProp.getProperty("gerrit.uri");
+        GERRIT_SCOPE = systemConfigProp.getProperty("gerrit.scope");
+        CLIENT_ID = systemConfigProp.getProperty("appengine.clientID");
+        ANALYTICS_ID = systemConfigProp.getProperty("analytics.id");
+
+        CoverageEntity.setPropertyValues(systemConfigProp);
+        TestSuiteResultEntity.setPropertyValues(systemConfigProp);
+        EmailHelper.setPropertyValues(systemConfigProp);
+        GcsHelper.setGcsProjectId(systemConfigProp.getProperty("gcs.projectID"));
+    }
+
+    @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();
+        Optional<User> currentUser = Optional.ofNullable(userService.getCurrentUser());
+        String currentUserEmail =
+                currentUser.isPresent()
+                        ? currentUser.map(user -> user.getEmail().trim()).orElse("")
+                        : "";
+        String requestUri = request.getRequestURI();
+        String requestArgs = request.getQueryString();
+        String loginURI = userService.createLoginURL(requestUri + '?' + requestArgs);
+        String logoutURI = userService.createLogoutURL(loginURI);
+        if (currentUserEmail != "") {
+
+            int activeIndex;
+            switch (getNavParentType()) {
+                case PROFILING_LIST:
+                    activeIndex = 3;
+                    break;
+                case COVERAGE_OVERVIEW:
+                    activeIndex = 2;
+                    break;
+                case RELEASE:
+                    activeIndex = 1;
+                    break;
+                default:
+                    activeIndex = 0;
+                    break;
+            }
+            if (request.getParameter(TREE_DEFAULT_PARAM) != null) {
+                HttpSession session = request.getSession(true);
+                boolean treeDefault = request.getParameter(TREE_DEFAULT_PARAM).equals("true");
+                session.setAttribute(TREE_DEFAULT_PARAM, treeDefault);
+            }
+
+            request.setAttribute("serverName", request.getServerName());
+            request.setAttribute("logoutURL", logoutURI);
+            request.setAttribute("email", currentUserEmail);
+            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");
+
+            if (currentUserEmail.endsWith("@google.com")
+                    || UserEntity.getUserList().contains(currentUserEmail)) {
+                doGetHandler(request, response);
+            } else {
+                RequestDispatcher dispatcher =
+                        request.getRequestDispatcher("WEB-INF/jsp/auth_error.jsp");
+                try {
+                    dispatcher.forward(request, response);
+                } catch (ServletException e) {
+                    logger.log(Level.SEVERE, "Servlet Exception caught : ", e);
+                }
+            }
+        } else {
+            response.sendRedirect(loginURI);
+        }
+    }
+
+    /**
+     * Implementation of the doGet method to be executed by servlet subclasses.
+     *
+     * @param request The HttpServletRequest object.
+     * @param response The HttpServletResponse object.
+     */
+    public abstract void doGetHandler(HttpServletRequest request, HttpServletResponse response)
+            throws IOException;
 }
diff --git a/src/main/java/com/android/vts/util/DatastoreHelper.java b/src/main/java/com/android/vts/util/DatastoreHelper.java
index 2dbca37..3764c7a 100644
--- a/src/main/java/com/android/vts/util/DatastoreHelper.java
+++ b/src/main/java/com/android/vts/util/DatastoreHelper.java
@@ -112,8 +112,8 @@
     DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
     Key startKey = KeyFactory.createKey(parentKey, kind, lowerBound);
     Filter startFilter =
-        new FilterPredicate(
-            Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, startKey);
+            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;
   }
@@ -132,7 +132,7 @@
     }
     Key endKey = KeyFactory.createKey(parentKey, kind, upperBound);
     Filter endFilter =
-        new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.LESS_THAN, endKey);
+            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;
   }
@@ -179,10 +179,10 @@
     List<Entity> profilingPointRunEntityList = new ArrayList<>();
 
     if (!report.hasStartTimestamp()
-        || !report.hasEndTimestamp()
-        || !report.hasTest()
-        || !report.hasHostInfo()
-        || !report.hasBuildInfo()) {
+            || !report.hasEndTimestamp()
+            || !report.hasTest()
+            || !report.hasHostInfo()
+            || !report.hasBuildInfo()) {
       // missing information
       return;
     }
@@ -195,8 +195,8 @@
     TestEntity testEntity = new TestEntity(testName);
 
     Key testRunKey =
-        KeyFactory.createKey(
-            testEntity.getOldKey(), TestRunEntity.KIND, report.getStartTimestamp());
+            KeyFactory.createKey(
+                    testEntity.getOldKey(), TestRunEntity.KIND, report.getStartTimestamp());
 
     long passCount = 0;
     long failCount = 0;
@@ -220,7 +220,7 @@
         ++failCount;
       }
       if (testCase.getSystraceCount() > 0
-          && testCase.getSystraceList().get(0).getUrlCount() > 0) {
+              && testCase.getSystraceList().get(0).getUrlCount() > 0) {
         String systraceLink = testCase.getSystraceList().get(0).getUrl(0).toStringUtf8();
         links.add(systraceLink);
       }
@@ -228,7 +228,7 @@
       // Process coverage data for test case
       for (CoverageReportMessage coverage : testCase.getCoverageList()) {
         CoverageEntity coverageEntity =
-            CoverageEntity.fromCoverageReport(testRunKey, testCaseName, coverage);
+                CoverageEntity.fromCoverageReport(testRunKey, testCaseName, coverage);
         if (coverageEntity == null) {
           logger.log(Level.WARNING, "Invalid coverage report in test run " + testRunKey);
         } else {
@@ -241,7 +241,7 @@
       // Process profiling data for test case
       for (ProfilingReportMessage profiling : testCase.getProfilingList()) {
         ProfilingPointRunEntity profilingPointRunEntity =
-            ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
+                ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
         if (profilingPointRunEntity == null) {
           logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey);
         } else {
@@ -275,7 +275,7 @@
     long testRunType = 0;
     for (AndroidDeviceInfoMessage device : report.getDeviceInfoList()) {
       DeviceInfoEntity deviceInfoEntity =
-          DeviceInfoEntity.fromDeviceInfoMessage(testRunKey, device);
+              DeviceInfoEntity.fromDeviceInfoMessage(testRunKey, device);
       if (deviceInfoEntity == null) {
         logger.log(Level.WARNING, "Invalid device info in test run " + testRunKey);
       } else {
@@ -308,7 +308,7 @@
     // Process global coverage data
     for (CoverageReportMessage coverage : report.getCoverageList()) {
       CoverageEntity coverageEntity =
-          CoverageEntity.fromCoverageReport(testRunKey, new String(), coverage);
+              CoverageEntity.fromCoverageReport(testRunKey, new String(), coverage);
       if (coverageEntity == null) {
         logger.log(Level.WARNING, "Invalid coverage report in test run " + testRunKey);
       } else {
@@ -322,19 +322,19 @@
     for (ApiCoverageReportMessage apiCoverage : report.getApiCoverageList()) {
       HalInterfaceMessage halInterfaceMessage = apiCoverage.getHalInterface();
       List<String> halApiList = apiCoverage.getHalApiList().stream().map(h -> h.toStringUtf8())
-          .collect(
-              Collectors.toList());
+              .collect(
+                      Collectors.toList());
       List<String> coveredHalApiList = apiCoverage.getCoveredHalApiList().stream()
-          .map(h -> h.toStringUtf8()).collect(
-              Collectors.toList());
+              .map(h -> h.toStringUtf8()).collect(
+                      Collectors.toList());
       ApiCoverageEntity apiCoverageEntity = new ApiCoverageEntity(
-          testRunKey,
-          halInterfaceMessage.getHalPackageName().toStringUtf8(),
-          halInterfaceMessage.getHalVersionMajor(),
-          halInterfaceMessage.getHalVersionMinor(),
-          halInterfaceMessage.getHalInterfaceName().toStringUtf8(),
-          halApiList,
-          coveredHalApiList
+              testRunKey,
+              halInterfaceMessage.getHalPackageName().toStringUtf8(),
+              halInterfaceMessage.getHalVersionMajor(),
+              halInterfaceMessage.getHalVersionMinor(),
+              halInterfaceMessage.getHalInterfaceName().toStringUtf8(),
+              halApiList,
+              coveredHalApiList
       );
       com.googlecode.objectify.Key apiCoverageEntityKey = apiCoverageEntity.save();
       if (apiCoverageEntityKey == null) {
@@ -345,7 +345,7 @@
     // Process global profiling data
     for (ProfilingReportMessage profiling : report.getProfilingList()) {
       ProfilingPointRunEntity profilingPointRunEntity =
-          ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
+              ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
       if (profilingPointRunEntity == null) {
         logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey);
       } else {
@@ -370,18 +370,18 @@
 
     boolean hasCodeCoverage = totalLineCount > 0 && coveredLineCount >= 0;
     TestRunEntity testRunEntity =
-        new TestRunEntity(
-            testEntity.getOldKey(),
-            testRunType,
-            startTimestamp,
-            endTimestamp,
-            testBuildId,
-            hostName,
-            passCount,
-            failCount,
-            hasCodeCoverage,
-            testCaseIds,
-            links);
+            new TestRunEntity(
+                    testEntity.getOldKey(),
+                    testRunType,
+                    startTimestamp,
+                    endTimestamp,
+                    testBuildId,
+                    hostName,
+                    passCount,
+                    failCount,
+                    hasCodeCoverage,
+                    testCaseIds,
+                    links);
     testEntityList.add(testRunEntity.toEntity());
 
     CodeCoverageEntity codeCoverageEntity = new CodeCoverageEntity(
@@ -394,11 +394,11 @@
 
     if (datastoreTransactionalRetry(test, testEntityList)) {
       List<List<Entity>> auxiliaryEntityList =
-          Arrays.asList(
-              profilingPointRunEntityList,
-              coverageEntityList,
-              branchEntityList,
-              buildTargetEntityList);
+              Arrays.asList(
+                      profilingPointRunEntityList,
+                      coverageEntityList,
+                      branchEntityList,
+                      buildTargetEntityList);
       int indexCount = 0;
       for (List<Entity> entityList : auxiliaryEntityList) {
         switch (indexCount) {
@@ -406,12 +406,12 @@
           case 1:
             if (entityList.size() > MAX_ENTITY_SIZE_PER_TRANSACTION) {
               List<List<Entity>> partitionedList =
-                  Lists.partition(entityList, MAX_ENTITY_SIZE_PER_TRANSACTION);
+                      Lists.partition(entityList, MAX_ENTITY_SIZE_PER_TRANSACTION);
               partitionedList.forEach(
-                  subEntityList -> {
-                    datastoreTransactionalRetry(
-                        new Entity(NULL_ENTITY_KIND), subEntityList);
-                  });
+                      subEntityList -> {
+                        datastoreTransactionalRetry(
+                                new Entity(NULL_ENTITY_KIND), subEntityList);
+                      });
             } else {
               datastoreTransactionalRetry(new Entity(NULL_ENTITY_KIND), entityList);
             }
@@ -419,7 +419,7 @@
           case 2:
           case 3:
             datastoreTransactionalRetryWithXG(
-                new Entity(NULL_ENTITY_KIND), entityList, true);
+                    new Entity(NULL_ENTITY_KIND), entityList, true);
             break;
           default:
             break;
@@ -429,7 +429,7 @@
 
       if (testRunEntity.getType() == TestRunType.POSTSUBMIT.getNumber()) {
         VtsAlertJobServlet.addTask(testRunKey);
-                if (testRunEntity.getHasCodeCoverage()) {
+        if (testRunEntity.getHasCodeCoverage()) {
           VtsCoverageAlertJobServlet.addTask(testRunKey);
         }
         if (profilingPointKeys.size() > 0) {
@@ -437,9 +437,9 @@
         }
       } else {
         logger.log(
-            Level.WARNING,
-            "The alert email was not sent as testRunEntity type is not POSTSUBMIT!" +
-                " \n " + " testRunEntity type => " + testRunEntity.getType());
+                Level.WARNING,
+                "The alert email was not sent as testRunEntity type is not POSTSUBMIT!" +
+                        " \n " + " testRunEntity type => " + testRunEntity.getType());
       }
     }
   }
@@ -506,18 +506,18 @@
       return;
     }
     TestPlanRunEntity testPlanRun =
-        new TestPlanRunEntity(
-            testPlanEntity.getKey(),
-            testPlanName,
-            type,
-            startTimestamp,
-            endTimestamp,
-            testBuildId,
-            passCount,
-            failCount,
-            0L,
-            0L,
-            testRunKeys);
+            new TestPlanRunEntity(
+                    testPlanEntity.getKey(),
+                    testPlanName,
+                    type,
+                    startTimestamp,
+                    endTimestamp,
+                    testBuildId,
+                    passCount,
+                    failCount,
+                    0L,
+                    0L,
+                    testRunKeys);
 
     // Create the device infos.
     for (DeviceInfoEntity device : deviceInfoEntitySet) {
@@ -549,7 +549,7 @@
    * @param entityList The list of entity for using datastore put method.
    */
   private static boolean datastoreTransactionalRetryWithXG(
-      Entity entity, List<Entity> entityList, boolean withXG) {
+          Entity entity, List<Entity> entityList, boolean withXG) {
     int retries = 0;
     while (true) {
       Transaction txn;
@@ -568,7 +568,7 @@
               Entity datastoreEntity = datastore.get(entity.getKey());
               TestEntity datastoreTestEntity = TestEntity.fromEntity(datastoreEntity);
               if (datastoreTestEntity == null
-                  || !datastoreTestEntity.equals(entity)) {
+                      || !datastoreTestEntity.equals(entity)) {
                 entityList.add(entity);
               }
             } else if (entity.getKind().equalsIgnoreCase("TestPlan")) {
@@ -584,25 +584,25 @@
         txn.commit();
         break;
       } catch (ConcurrentModificationException
-          | DatastoreFailureException
-          | DatastoreTimeoutException e) {
+              | DatastoreFailureException
+              | DatastoreTimeoutException e) {
         entityList.remove(entity);
         logger.log(
-            Level.WARNING,
-            "Retrying insert kind: " + entity.getKind() + " key: " + entity.getKey());
+                Level.WARNING,
+                "Retrying insert kind: " + entity.getKind() + " key: " + entity.getKey());
         if (retries++ >= MAX_WRITE_RETRIES) {
           logger.log(
-              Level.SEVERE,
-              "Exceeded maximum retries kind: "
-                  + entity.getKind()
-                  + " key: "
-                  + entity.getKey());
+                  Level.SEVERE,
+                  "Exceeded maximum retries kind: "
+                          + entity.getKind()
+                          + " key: "
+                          + entity.getKey());
           return false;
         }
       } finally {
         if (txn.isActive()) {
           logger.log(
-              Level.WARNING, "Transaction rollback forced for : " + entity.getKind());
+                  Level.WARNING, "Transaction rollback forced for : " + entity.getKind());
           txn.rollback();
         }
       }