allow inserting arbitary attributes into Result XML tag

This change enables arbitrary key-value pairs to be added to
the "Result" XML tag in CTS result XML as attributes and their
corresponding values.

Also included a minor fix to the Java version detection in
cts-tradefed: the previous implementation assumed that the
version number will be printed on the first line.

Bug: 148189589
Test: cts-tradefed run cts -m CtsGestureTestCases \
        --result-attribute display_mode 0
Change-Id: I5a2facbb415f879cc2f57792e5ddea35dede58b9
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
index 2267720..227fcf5 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/ResultReporter.java
@@ -151,6 +151,13 @@
             description = "Whether failure summary report is included in the zip fie.")
     private boolean mIncludeHtml = false;
 
+    @Option(
+            name = "result-attribute",
+            description =
+                    "Extra key-value pairs to be added as attributes and corresponding"
+                            + "values of the \"Result\" tag in the result XML.")
+    private Map<String, String> mResultAttributes = new HashMap<String, String>();
+
     private CompatibilityBuildHelper mBuildHelper;
     private File mResultDir = null;
     private File mLogDir = null;
@@ -831,11 +838,19 @@
      */
     protected File generateResultXmlFile()
             throws IOException, XmlPullParserException {
-        return ResultHandler.writeResults(mBuildHelper.getSuiteName(),
-                mBuildHelper.getSuiteVersion(), getSuitePlan(mBuildHelper),
-                mBuildHelper.getSuiteBuild(), mResult, mResultDir, mResult.getStartTime(),
-                mElapsedTime + mResult.getStartTime(), mReferenceUrl, getLogUrl(),
-                mBuildHelper.getCommandLineArgs());
+        return ResultHandler.writeResults(
+                mBuildHelper.getSuiteName(),
+                mBuildHelper.getSuiteVersion(),
+                getSuitePlan(mBuildHelper),
+                mBuildHelper.getSuiteBuild(),
+                mResult,
+                mResultDir,
+                mResult.getStartTime(),
+                mElapsedTime + mResult.getStartTime(),
+                mReferenceUrl,
+                getLogUrl(),
+                mBuildHelper.getCommandLineArgs(),
+                mResultAttributes);
     }
 
     /**
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationResultXml.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationResultXml.java
index 2e31722..34dc6f6 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationResultXml.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationResultXml.java
@@ -25,6 +25,9 @@
 import org.xmlpull.v1.XmlSerializer;
 
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Utility class to save a Compatibility run as an XML.
@@ -46,27 +49,29 @@
     private String mSuiteBuild;
     private String mReferenceUrl;
     private String mLogUrl;
+    private Map<String, String> mResultAttributes = new HashMap<String, String>();
 
     /**
      * Empty version of the constructor when loading results.
      */
     public CertificationResultXml() {}
 
-    /**
-     * Create an XML report specialized for the Compatibility Test cases.
-     */
-    public CertificationResultXml(String suiteName,
+    /** Create an XML report specialized for the Compatibility Test cases. */
+    public CertificationResultXml(
+            String suiteName,
             String suiteVersion,
             String suitePlan,
             String suiteBuild,
             String referenceUrl,
-            String logUrl) {
+            String logUrl,
+            Map<String, String> resultAttributes) {
         mSuiteName = suiteName;
         mSuiteVersion = suiteVersion;
         mSuitePlan = suitePlan;
         mSuiteBuild = suiteBuild;
         mReferenceUrl = referenceUrl;
         mLogUrl = logUrl;
+        mResultAttributes = resultAttributes;
     }
 
     /**
@@ -88,6 +93,12 @@
         if (mLogUrl != null) {
             serializer.attribute(NS, LOG_URL_ATTR, mLogUrl);
         }
+
+        if (mResultAttributes != null) {
+            for (Entry<String, String> entry : mResultAttributes.entrySet()) {
+                serializer.attribute(NS, entry.getKey(), entry.getValue());
+            }
+        }
     }
 
     @Override
diff --git a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java
index 88a62a8..ae40911 100644
--- a/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java
+++ b/common/host-side/tradefed/src/com/android/compatibility/common/tradefed/result/suite/CertificationSuiteResultReporter.java
@@ -106,6 +106,13 @@
             description = "Whether failure summary report is included in the zip fie.")
     private boolean mIncludeHtml = false;
 
+    @Option(
+            name = "result-attribute",
+            description =
+                    "Extra key-value pairs to be added as attributes and corresponding values "
+                            + "of the \"Result\" tag in the result XML.")
+    private Map<String, String> mResultAttributes = new HashMap<String, String>();
+
     private CompatibilityBuildHelper mBuildHelper;
 
     /** The directory containing the results */
@@ -295,12 +302,14 @@
 
     @Override
     public IFormatterGenerator createFormatter() {
-        return new CertificationResultXml(mBuildHelper.getSuiteName(),
+        return new CertificationResultXml(
+                mBuildHelper.getSuiteName(),
                 mBuildHelper.getSuiteVersion(),
                 mBuildHelper.getSuitePlan(),
                 mBuildHelper.getSuiteBuild(),
                 mReferenceUrl,
-                getLogUrl());
+                getLogUrl(),
+                mResultAttributes);
     }
 
     @Override
diff --git a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/SubPlanHelperTest.java b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/SubPlanHelperTest.java
index 005e180..334985e 100644
--- a/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/SubPlanHelperTest.java
+++ b/common/host-side/tradefed/tests/src/com/android/compatibility/common/tradefed/result/SubPlanHelperTest.java
@@ -186,9 +186,19 @@
         moduleBTest4.setResultStatus(TestStatus.PASS);
 
         // Serialize to file
-        ResultHandler.writeResults(SUITE_NAME, SUITE_VERSION, SUITE_PLAN, SUITE_BUILD,
-                result, mResultDir, START_MS, END_MS, REFERENCE_URL, LOG_URL,
-                COMMAND_LINE_ARGS);
+        ResultHandler.writeResults(
+                SUITE_NAME,
+                SUITE_VERSION,
+                SUITE_PLAN,
+                SUITE_BUILD,
+                result,
+                mResultDir,
+                START_MS,
+                END_MS,
+                REFERENCE_URL,
+                LOG_URL,
+                COMMAND_LINE_ARGS,
+                null);
     }
 
     private class SpctMockCompatibilityBuildHelper extends CompatibilityBuildHelper {
diff --git a/common/util/src/com/android/compatibility/common/util/ResultHandler.java b/common/util/src/com/android/compatibility/common/util/ResultHandler.java
index edf457c..1e57c32 100644
--- a/common/util/src/com/android/compatibility/common/util/ResultHandler.java
+++ b/common/util/src/com/android/compatibility/common/util/ResultHandler.java
@@ -42,6 +42,7 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
@@ -367,15 +368,26 @@
      * @param referenceUrl A nullable string that can contain a URL to a related data
      * @param logUrl A nullable string that can contain a URL to related log files
      * @param commandLineArgs A string containing the arguments to the run command
+     * @param resultAttributes Extra key-value pairs to be added as attributes and corresponding
+     *     values into the result XML file
      * @return The result file created.
      * @throws IOException
      * @throws XmlPullParserException
      */
-    public static File writeResults(String suiteName, String suiteVersion, String suitePlan,
-            String suiteBuild, IInvocationResult result, File resultDir,
-            long startTime, long endTime, String referenceUrl, String logUrl,
-            String commandLineArgs)
-                    throws IOException, XmlPullParserException {
+    public static File writeResults(
+            String suiteName,
+            String suiteVersion,
+            String suitePlan,
+            String suiteBuild,
+            IInvocationResult result,
+            File resultDir,
+            long startTime,
+            long endTime,
+            String referenceUrl,
+            String logUrl,
+            String commandLineArgs,
+            Map<String, String> resultAttributes)
+            throws IOException, XmlPullParserException {
         int passed = result.countResults(TestStatus.PASS);
         int failed = result.countResults(TestStatus.FAIL);
         File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
@@ -399,6 +411,12 @@
         serializer.attribute(NS, REPORT_VERSION_ATTR, RESULT_FILE_VERSION);
         serializer.attribute(NS, COMMAND_LINE_ARGS, nullToEmpty(commandLineArgs));
 
+        if (resultAttributes != null) {
+            for (Entry<String, String> entry : resultAttributes.entrySet()) {
+                serializer.attribute(NS, entry.getKey(), entry.getValue());
+            }
+        }
+
         if (referenceUrl != null) {
             serializer.attribute(NS, REFERENCE_URL_ATTR, referenceUrl);
         }
diff --git a/common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java b/common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
index be22964..7b66c20 100644
--- a/common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
+++ b/common/util/tests/src/com/android/compatibility/common/util/ResultHandlerTest.java
@@ -34,6 +34,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -239,6 +240,9 @@
         ITestResult moduleBTest5 = moduleBCase.getOrCreateResult(METHOD_5);
         moduleBTest5.skipped();
 
+        Map<String, String> testAttributes = new HashMap<String, String>();
+        testAttributes.put("foo1", "bar1");
+        testAttributes.put("foo2", "bar2");
         // Serialize to file
         File res =
                 ResultHandler.writeResults(
@@ -252,8 +256,11 @@
                         END_MS,
                         REFERENCE_URL,
                         LOG_URL,
-                        COMMAND_LINE_ARGS);
+                        COMMAND_LINE_ARGS,
+                        testAttributes);
         String content = FileUtil.readStringFromFile(res);
+        assertXmlContainsAttribute(content, "Result", "foo1", "bar1");
+        assertXmlContainsAttribute(content, "Result", "foo2", "bar2");
         assertXmlContainsAttribute(content, "Result/Build", "run_history", EXAMPLE_RUN_HISTORY);
         assertXmlContainsNode(content, "Result/RunHistory");
         assertXmlContainsAttribute(content, "Result/RunHistory/Run", "start", "10000000000000");
@@ -336,7 +343,8 @@
                         END_MS,
                         REFERENCE_URL,
                         LOG_URL,
-                        COMMAND_LINE_ARGS);
+                        COMMAND_LINE_ARGS,
+                        null);
         String content = FileUtil.readStringFromFile(res);
         assertXmlContainsNode(content, "Result/Module/TestCase/Test/RunHistory");
         assertXmlContainsAttribute(
diff --git a/tools/cts-tradefed/etc/cts-tradefed b/tools/cts-tradefed/etc/cts-tradefed
index e2833e8..830c395 100755
--- a/tools/cts-tradefed/etc/cts-tradefed
+++ b/tools/cts-tradefed/etc/cts-tradefed
@@ -41,7 +41,7 @@
 checkPath java
 
 # check java version
-JAVA_VERSION=$(java -version 2>&1 | head -n 1 | grep 'version [ "]\(1\.8\|9\|11\).*[ "]')
+JAVA_VERSION=$(java -version 2>&1 | grep -m 1 'version [ "]\(1\.8\|9\|11\).*[ "]')
 if [ "${JAVA_VERSION}" == "" ]; then
     echo "Wrong java version. 1.8, 9, or 11 is required."
     exit