manual cherry-pick of 45f8e6a523a096a3781f90d4fd5d54dcc0673a7e

Change-Id: I770948540f3aca9d930edea7a12119ff63fe9bdf
diff --git a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
index d360c40..a54ee4c 100644
--- a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
+++ b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/DeviceInfoInstrument.java
@@ -20,14 +20,28 @@
 import android.app.Instrumentation;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.FeatureInfo;
+import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.telephony.TelephonyManager;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.Display;
 import android.view.WindowManager;
 
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 public class DeviceInfoInstrument extends Instrumentation {
+
+    private static final String TAG = "DeviceInfoInstrument";
+
+    private static final String PROCESSES = "processes";
+    private static final String FEATURES = "features";
     private static final String PHONE_NUMBER = "phoneNumber";
     public static final String LOCALES = "locales";
     private static final String IMSI = "imsi";
@@ -122,6 +136,14 @@
         String phoneNumber = tm.getLine1Number();
         addResult(PHONE_NUMBER, phoneNumber);
 
+        // features
+        String features = getFeatures();
+        addResult(FEATURES, features);
+
+        // processes
+        String processes = getProcesses();
+        addResult(PROCESSES, processes);
+
         finish(Activity.RESULT_OK, mResults);
     }
 
@@ -154,4 +176,84 @@
     private void addResult(final String key, final float value){
         mResults.putFloat(key, value);
     }
+
+    /**
+     * Return a summary of the device's feature as a semi-colon-delimited list of colon separated
+     * name and availability pairs like "feature1:sdk:true;feature2:sdk:false;feature3:other:true;".
+     */
+    private String getFeatures() {
+        StringBuilder features = new StringBuilder();
+
+        try {
+            Set<String> checkedFeatures = new HashSet<String>();
+
+            PackageManager packageManager = getContext().getPackageManager();
+            for (String featureName : getPackageManagerFeatures()) {
+                checkedFeatures.add(featureName);
+                boolean hasFeature = packageManager.hasSystemFeature(featureName);
+                addFeature(features, featureName, "sdk", hasFeature);
+            }
+
+            FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures();
+            if (featureInfos != null) {
+                for (FeatureInfo featureInfo : featureInfos) {
+                    if (featureInfo.name != null && !checkedFeatures.contains(featureInfo.name)) {
+                        addFeature(features, featureInfo.name, "other", true);
+                    }
+                }
+            }
+        } catch (Exception exception) {
+            Log.e(TAG, "Error getting features: " + exception.getMessage(), exception);
+        }
+
+        return features.toString();
+    }
+
+    private static void addFeature(StringBuilder features, String name, String type,
+            boolean available) {
+        features.append(name).append(':').append(type).append(':').append(available).append(';');
+    }
+
+    /**
+     * Use reflection to get the features defined by the SDK. If there are features that do not fit
+     * the convention of starting with "FEATURE_" then they will still be shown under the
+     * "Other Features" section.
+     *
+     * @return list of feature names from sdk
+     */
+    private List<String> getPackageManagerFeatures() {
+        try {
+            List<String> features = new ArrayList<String>();
+            Field[] fields = PackageManager.class.getFields();
+            for (Field field : fields) {
+                if (field.getName().startsWith("FEATURE_")) {
+                    String feature = (String) field.get(null);
+                    features.add(feature);
+                }
+            }
+            return features;
+        } catch (IllegalAccessException illegalAccess) {
+            throw new RuntimeException(illegalAccess);
+        }
+    }
+
+    /**
+     * Return a semi-colon-delimited list of the root processes that were running on the phone
+     * or an error message.
+     */
+    private static String getProcesses() {
+        StringBuilder builder = new StringBuilder();
+
+        try {
+            String[] rootProcesses = RootProcessScanner.getRootProcesses();
+            for (String rootProcess : rootProcesses) {
+                builder.append(rootProcess).append(';');
+            }
+        } catch (Exception exception) {
+            Log.e(TAG, "Error getting processes: " + exception.getMessage(), exception);
+            builder.append(exception.getMessage());
+        }
+
+        return builder.toString();
+    }
 }
diff --git a/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/RootProcessScanner.java b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/RootProcessScanner.java
new file mode 100644
index 0000000..4763287
--- /dev/null
+++ b/tools/device-setup/TestDeviceSetup/src/android/tests/getinfo/RootProcessScanner.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2010 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 android.tests.getinfo;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+/** Crawls /proc to find processes that are running as root. */
+class RootProcessScanner {
+
+    /** Processes that are allowed to run as root. */
+    private static final Pattern ROOT_PROCESS_WHITELIST_PATTERN = getRootProcessWhitelistPattern(
+            "debuggerd",
+            "init",
+            "installd",
+            "servicemanager",
+            "vold",
+            "zygote"
+    );
+
+    /** Combine the individual patterns into one super pattern. */
+    private static Pattern getRootProcessWhitelistPattern(String... patterns) {
+        StringBuilder rootProcessPattern = new StringBuilder();
+        for (int i = 0; i < patterns.length; i++) {
+            rootProcessPattern.append(patterns[i]);
+            if (i + 1 < patterns.length) {
+                rootProcessPattern.append('|');
+            }
+        }
+        return Pattern.compile(rootProcessPattern.toString());
+    }
+
+    /** Test that there are no unapproved root processes running on the system. */
+    public static String[] getRootProcesses()
+            throws FileNotFoundException, MalformedStatMException {
+        List<File> rootProcessDirs = getRootProcessDirs();
+        String[] rootProcessNames = new String[rootProcessDirs.size()];
+        for (int i = 0; i < rootProcessNames.length; i++) {
+            rootProcessNames[i] = getProcessName(rootProcessDirs.get(i));
+        }
+        return rootProcessNames;
+    }
+
+    private static List<File> getRootProcessDirs()
+            throws FileNotFoundException, MalformedStatMException {
+        File proc = new File("/proc");
+        if (!proc.exists()) {
+            throw new FileNotFoundException(proc + " is missing (man 5 proc)");
+        }
+
+        List<File> rootProcesses = new ArrayList<File>();
+        File[] processDirs = proc.listFiles();
+        if (processDirs != null && processDirs.length > 0) {
+            for (File processDir : processDirs) {
+                if (isUnapprovedRootProcess(processDir)) {
+                    rootProcesses.add(processDir);
+                }
+            }
+        }
+        return rootProcesses;
+    }
+
+    /**
+     * Filters out processes in /proc that are not approved.
+     * @throws FileNotFoundException
+     * @throws MalformedStatMException
+     */
+    private static boolean isUnapprovedRootProcess(File pathname)
+            throws FileNotFoundException, MalformedStatMException {
+        return isPidDirectory(pathname)
+                && !isKernelProcess(pathname)
+                && isRootProcess(pathname);
+    }
+
+    private static boolean isPidDirectory(File pathname) {
+        return pathname.isDirectory() && Pattern.matches("\\d+", pathname.getName());
+    }
+
+    private static boolean isKernelProcess(File processDir)
+            throws FileNotFoundException, MalformedStatMException {
+        File statm = getProcessStatM(processDir);
+        Scanner scanner = null;
+        try {
+            scanner = new Scanner(statm);
+
+            boolean allZero = true;
+            for (int i = 0; i < 7; i++) {
+                if (scanner.nextInt() != 0) {
+                    allZero = false;
+                }
+            }
+
+            if (scanner.hasNext()) {
+                throw new MalformedStatMException(processDir
+                        + " statm expected to have 7 integers (man 5 proc)");
+            }
+
+            return allZero;
+        } finally {
+            if (scanner != null) {
+                scanner.close();
+            }
+        }
+    }
+
+    private static File getProcessStatM(File processDir) {
+        return new File(processDir, "statm");
+    }
+
+    public static class MalformedStatMException extends Exception {
+        MalformedStatMException(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+
+    /**
+     * Return whether or not this process is running as root without being approved.
+     *
+     * @param processDir with the status file
+     * @return whether or not it is a unwhitelisted root process
+     * @throws FileNotFoundException
+     */
+    private static boolean isRootProcess(File processDir) throws FileNotFoundException {
+        File status = getProcessStatus(processDir);
+        Scanner scanner = null;
+        try {
+            scanner = new Scanner(status);
+
+            scanner = findToken(scanner, "Name:");
+            String name = scanner.next();
+
+            scanner = findToken(scanner, "Uid:");
+            boolean rootUid = hasRootId(scanner);
+
+            scanner = findToken(scanner, "Gid:");
+            boolean rootGid = hasRootId(scanner);
+
+            return !ROOT_PROCESS_WHITELIST_PATTERN.matcher(name).matches()
+                    && (rootUid || rootGid);
+        } finally {
+            if (scanner != null) {
+                scanner.close();
+            }
+        }
+    }
+
+    /**
+     * Get the status {@link File} that has name:value pairs.
+     * <pre>
+     * Name:   init
+     * ...
+     * Uid:    0       0       0       0
+     * Gid:    0       0       0       0
+     * </pre>
+     */
+    private static File getProcessStatus(File processDir) {
+        return new File(processDir, "status");
+    }
+
+    /**
+     * Convenience method to move the scanner's position to the point after the given token.
+     *
+     * @param scanner to call next() until the token is found
+     * @param token to find like "Name:"
+     * @return scanner after finding token
+     */
+    private static Scanner findToken(Scanner scanner, String token) {
+        while (true) {
+            String next = scanner.next();
+            if (next.equals(token)) {
+                return scanner;
+            }
+        }
+
+        // Scanner will exhaust input and throw an exception before getting here.
+    }
+
+    /**
+     * Uid and Gid lines have four values: "Uid:    0       0       0       0"
+     *
+     * @param scanner that has just processed the "Uid:" or "Gid:" token
+     * @return whether or not any of the ids are root
+     */
+    private static boolean hasRootId(Scanner scanner) {
+        int realUid = scanner.nextInt();
+        int effectiveUid = scanner.nextInt();
+        int savedSetUid = scanner.nextInt();
+        int fileSystemUid = scanner.nextInt();
+        return realUid == 0 || effectiveUid == 0 || savedSetUid == 0 || fileSystemUid == 0;
+    }
+
+    /** Returns the name of the process corresponding to its process directory in /proc. */
+    private static String getProcessName(File processDir) throws FileNotFoundException {
+        File status = getProcessStatus(processDir);
+        Scanner scanner = new Scanner(status);
+        try {
+            scanner = findToken(scanner, "Name:");
+            return scanner.next();
+        } finally {
+            scanner.close();
+        }
+    }
+}
diff --git a/tools/host/src/com/android/cts/ConsoleUi.java b/tools/host/src/com/android/cts/ConsoleUi.java
index 27173a4..679ecc0 100644
--- a/tools/host/src/com/android/cts/ConsoleUi.java
+++ b/tools/host/src/com/android/cts/ConsoleUi.java
@@ -970,7 +970,7 @@
      * @param resultType The result type.
      */
     private void createPlanFromSession(final String name, TestSession ts, final String resultType)
-            throws FileNotFoundException, ParserConfigurationException,
+            throws FileNotFoundException, IOException, ParserConfigurationException,
             TransformerFactoryConfigurationError, TransformerException {
 
         HashMap<String, ArrayList<String>> selectedResult =
diff --git a/tools/host/src/com/android/cts/TestDevice.java b/tools/host/src/com/android/cts/TestDevice.java
index e384824..690742d 100644
--- a/tools/host/src/com/android/cts/TestDevice.java
+++ b/tools/host/src/com/android/cts/TestDevice.java
@@ -418,6 +418,8 @@
         public static final String IMEI = "imei";
         public static final String IMSI = "imsi";
         public static final String PHONE_NUMBER = "phoneNumber";
+        public static final String FEATURES = "features";
+        public static final String PROCESSES = "processes";
 
         private HashMap<String, String> mInfoMap;
 
@@ -785,6 +787,24 @@
         public String getPhoneNumber() {
             return mInfoMap.get(PHONE_NUMBER);
         }
+
+        /**
+         * Get features.
+         *
+         * @return Features.
+         */
+        public String getFeatures() {
+            return mInfoMap.get(FEATURES);
+        }
+
+        /**
+         * Get processes.
+         *
+         * @return Processes.
+         */
+        public String getProcesses() {
+            return mInfoMap.get(PROCESSES);
+        }
     }
 
     /**
diff --git a/tools/host/src/com/android/cts/TestSessionBuilder.java b/tools/host/src/com/android/cts/TestSessionBuilder.java
index c592476..b4e69f1 100644
--- a/tools/host/src/com/android/cts/TestSessionBuilder.java
+++ b/tools/host/src/com/android/cts/TestSessionBuilder.java
@@ -544,7 +544,7 @@
      */
     public void serialize(String planName,
             ArrayList<String> packageNames, HashMap<String, ArrayList<String>> selectedResult)
-            throws ParserConfigurationException, FileNotFoundException,
+            throws ParserConfigurationException, FileNotFoundException, IOException,
             TransformerFactoryConfigurationError, TransformerException {
         File plan = new File(HostConfig.getInstance().getPlanRepository()
                 .getPlanPath(planName));
diff --git a/tools/host/src/com/android/cts/TestSessionLog.java b/tools/host/src/com/android/cts/TestSessionLog.java
index f874cae..b37e5a2 100644
--- a/tools/host/src/com/android/cts/TestSessionLog.java
+++ b/tools/host/src/com/android/cts/TestSessionLog.java
@@ -69,6 +69,9 @@
     static final String ATTRIBUTE_BUILD_NAME = "buildName";
     static final String ATTRIBUTE_ARCH = "arch";
     static final String ATTRIBUTE_VALUE = "value";
+    static final String ATTRIBUTE_AVAILABLE = "available";
+    static final String ATTRIBUTE_TYPE = "type";
+    static final String ATTRIBUTE_UID = "uid";
 
     static final String ATTRIBUTE_PASS = "pass";
     static final String ATTRIBUTE_FAILED = "failed";
@@ -84,6 +87,10 @@
     static final String TAG_SUMMARY = "Summary";
     static final String TAG_SCREEN = "Screen";
     static final String TAG_BUILD_INFO = "BuildInfo";
+    static final String TAG_FEATURE_INFO = "FeatureInfo";
+    static final String TAG_FEATURE = "Feature";
+    static final String TAG_PROCESS_INFO = "ProcessInfo";
+    static final String TAG_PROCESS = "Process";
     static final String TAG_PHONE_SUB_INFO = "PhoneSubInfo";
     static final String TAG_TEST_RESULT = "TestResult";
     static final String TAG_TESTPACKAGE = "TestPackage";
@@ -327,6 +334,9 @@
                         DeviceParameterCollector.BUILD_ABI2, bldInfo.getBuildAbi2());
 
                 deviceSettingNode.appendChild(devInfoNode);
+
+                addFeatureInfo(doc, deviceSettingNode, bldInfo);
+                addProcessInfo(doc, deviceSettingNode, bldInfo);
             }
 
             Node hostInfo = doc.createElement(TAG_HOSTINFO);
@@ -393,6 +403,86 @@
     }
 
     /**
+     * Creates a {@link #TAG_FEATURE_INFO} tag with {@link #TAG_FEATURE} elements indicating
+     * what features are supported by the device. It parses a string from the deviceInfo argument
+     * that is in the form of "feature1:true;feature2:false;featuer3;true;" with a trailing
+     * semi-colon.
+     *
+     * <pre>
+     *  <FeatureInfo>
+     *     <Feature name="android.name.of.feature" available="true" />
+     *     ...
+     *   </FeatureInfo>
+     * </pre>
+     * @param document used to create elements
+     * @param parentNode to attach the FeatureInfo element to
+     * @param deviceInfo to get the feature data from
+     */
+    private void addFeatureInfo(Document document, Node parentNode,
+            DeviceParameterCollector deviceInfo) {
+        Node featureInfo = document.createElement(TAG_FEATURE_INFO);
+        parentNode.appendChild(featureInfo);
+
+        String features = deviceInfo.getFeatures();
+        if (features == null) {
+            features = "";
+        }
+
+        String[] featurePairs = features.split(";");
+        for (String featurePair : featurePairs) {
+            String[] nameTypeAvailability = featurePair.split(":");
+            if (nameTypeAvailability.length >= 3) {
+                Node feature = document.createElement(TAG_FEATURE);
+                featureInfo.appendChild(feature);
+
+                setAttribute(document, feature, ATTRIBUTE_NAME, nameTypeAvailability[0]);
+                setAttribute(document, feature, ATTRIBUTE_TYPE, nameTypeAvailability[1]);
+                setAttribute(document, feature, ATTRIBUTE_AVAILABLE, nameTypeAvailability[2]);
+            }
+        }
+    }
+
+    /**
+     * Creates a {@link #TAG_PROCESS_INFO} tag with {@link #TAG_PROCESS} elements indicating
+     * what particular processes of interest were running on the device. It parses a string from
+     * the deviceInfo argument that is in the form of "processName1;processName2;..." with a
+     * trailing semi-colon.
+     *
+     * <pre>
+     *   <ProcessInfo>
+     *     <Process name="long_cat_viewer" uid="0" />
+     *     ...
+     *   </ProcessInfo>
+     * </pre>
+     *
+     * @param document
+     * @param parentNode
+     * @param deviceInfo
+     */
+    private void addProcessInfo(Document document, Node parentNode,
+            DeviceParameterCollector deviceInfo) {
+        Node processInfo = document.createElement(TAG_PROCESS_INFO);
+        parentNode.appendChild(processInfo);
+
+        String rootProcesses = deviceInfo.getProcesses();
+        if (rootProcesses == null) {
+            rootProcesses = "";
+        }
+
+        String[] processNames = rootProcesses.split(";");
+        for (String processName : processNames) {
+            processName = processName.trim();
+            if (processName.length() > 0) {
+                Node process = document.createElement(TAG_PROCESS);
+                processInfo.appendChild(process);
+
+                setAttribute(document, process, ATTRIBUTE_NAME, processName);
+                setAttribute(document, process, ATTRIBUTE_UID, "0");
+            }
+        }
+    }
+
+    /**
      * Output TestSuite and result to XML DOM Document.
      *
      * @param doc The document.
@@ -443,7 +533,7 @@
                     testNode.appendChild(failedMessageNode);
                     setAttribute(doc, failedMessageNode,TAG_FAILED_MESSAGE, failedMessage);
 
-                    String stackTrace = result.getStackTrace();
+                    String stackTrace = sanitizeStackTrace(result.getStackTrace());
                     if (stackTrace != null) {
                         Node stackTraceNode = doc.createElement(TAG_STACK_TRACE);
                         failedMessageNode.appendChild(stackTraceNode);
@@ -462,6 +552,18 @@
     }
 
     /**
+     * Strip out any invalid XML characters that might cause the report to be unviewable.
+     * http://www.w3.org/TR/REC-xml/#dt-character
+     */
+    private static String sanitizeStackTrace(String trace) {
+        if (trace != null) {
+            return trace.replaceAll("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]", "");
+        } else {
+            return null;
+        }
+    }
+
+    /**
      * Fetch failed file name and line number
      *
      * @param failedResult failed message
diff --git a/tools/host/src/com/android/cts/XMLResourceHandler.java b/tools/host/src/com/android/cts/XMLResourceHandler.java
index f77b27b..f44a574 100644
--- a/tools/host/src/com/android/cts/XMLResourceHandler.java
+++ b/tools/host/src/com/android/cts/XMLResourceHandler.java
@@ -18,6 +18,7 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.IOException;
 
 import javax.xml.transform.Transformer;
 import javax.xml.transform.TransformerException;
@@ -125,11 +126,16 @@
      * @param doc DOM Document
      */
     protected static void writeToFile(File file, Document doc) throws FileNotFoundException,
-            TransformerFactoryConfigurationError, TransformerException {
+            IOException, TransformerFactoryConfigurationError, TransformerException {
         Transformer t = TransformerFactory.newInstance().newTransformer();
         // enable indent in result file
         t.setOutputProperty("indent", "yes");
-        t.transform(new DOMSource(doc),
-                new StreamResult(new FileOutputStream(file)));
+        FileOutputStream fos = new FileOutputStream(file);
+        try {
+            StreamResult sr = new StreamResult(fos);
+            t.transform(new DOMSource(doc), sr);
+        } finally {
+            fos.close();
+        }
     }
 }
diff --git a/tools/host/src/res/cts_result.css b/tools/host/src/res/cts_result.css
index 869c4dd..b5b4009 100644
--- a/tools/host/src/res/cts_result.css
+++ b/tools/host/src/res/cts_result.css
@@ -66,6 +66,7 @@
     border-color: gray;
     border-style: inset;
     font-size:1em;
+    vertical-align: top;
 }
 
 #summaryinfo table {
@@ -79,6 +80,7 @@
 #summaryinfo td {
     padding:1px;
     border-width: 0px 0px 0px 0px;
+    vertical-align: top;
 }
 
 /* The test summary */
diff --git a/tools/host/src/res/cts_result.xsl b/tools/host/src/res/cts_result.xsl
index 8f5f77c..72b0f86 100644
--- a/tools/host/src/res/cts_result.xsl
+++ b/tools/host/src/res/cts_result.xsl
@@ -116,7 +116,9 @@
                                     <TR>
                                         <TD class="rowtitle">Supported Locales</TD>
                                         <TD>
-                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@locales"/>
+                                            <xsl:call-template name="formatDelimitedString">
+                                                <xsl:with-param name="string" select="TestResult/DeviceInfo/BuildInfo/@locales"/>
+                                            </xsl:call-template>
                                         </TD>
                                     </TR>
                                     <TR>
@@ -179,6 +181,46 @@
                                             <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@imsi"/>
                                         </TD>
                                     </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Features</TD>
+                                        <TD>
+                                            <xsl:for-each select="TestResult/DeviceInfo/FeatureInfo/Feature[@type='sdk']">
+                                                <xsl:text>[</xsl:text>
+                                                <xsl:choose>
+                                                    <xsl:when test="@available = 'true'">
+                                                        <xsl:text>X</xsl:text>
+                                                    </xsl:when>
+                                                    <xsl:otherwise>
+                                                        <xsl:text>_</xsl:text>
+                                                    </xsl:otherwise>
+                                                </xsl:choose>
+                                                <xsl:text>] </xsl:text>
+
+                                                <xsl:value-of select="@name" />
+                                                <br />
+                                            </xsl:for-each>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Other Features</TD>
+                                        <TD>
+                                            <UL>
+                                                <xsl:for-each select="TestResult/DeviceInfo/FeatureInfo/Feature[@type='other']">
+                                                    <LI><xsl:value-of select="@name" /></LI>
+                                                </xsl:for-each>
+                                            </UL>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Root Processes</TD>
+                                        <TD>
+                                            <UL>
+                                                <xsl:for-each select="TestResult/DeviceInfo/ProcessInfo/Process[@uid='0']">
+                                                    <LI><xsl:value-of select="@name" /></LI>
+                                                </xsl:for-each>
+                                            </UL>
+                                        </TD>
+                                    </TR>
                                 </TABLE>
                             </div>
                         </TD>
@@ -417,10 +459,31 @@
                     </TABLE>
                 </xsl:for-each> <!-- end test package -->
             </DIV>
+            </body>
+        </html>
+    </xsl:template>
 
-        </body>
-    </html>
-</xsl:template>
+    <!-- Take a delimited string and insert line breaks after a some number of elements. --> 
+    <xsl:template name="formatDelimitedString">
+        <xsl:param name="string" />
+        <xsl:param name="numTokensPerRow" select="10" />
+        <xsl:param name="tokenIndex" select="1" />
+        <xsl:if test="$string">
+            <!-- Requires the last element to also have a delimiter after it. -->
+            <xsl:variable name="token" select="substring-before($string, ';')" />
+            <xsl:value-of select="$token" />
+            <xsl:text>&#160;</xsl:text>
+          
+            <xsl:if test="$tokenIndex mod $numTokensPerRow = 0">
+                <br />
+            </xsl:if>
 
+            <xsl:call-template name="formatDelimitedString">
+                <xsl:with-param name="string" select="substring-after($string, ';')" />
+                <xsl:with-param name="numTokensPerRow" select="$numTokensPerRow" />
+                <xsl:with-param name="tokenIndex" select="$tokenIndex + 1" />
+            </xsl:call-template>
+        </xsl:if>
+    </xsl:template>
 
 </xsl:stylesheet>
diff --git a/tools/utils/buildCts.py b/tools/utils/buildCts.py
index b3e9183..b1a6700 100755
--- a/tools/utils/buildCts.py
+++ b/tools/utils/buildCts.py
@@ -121,6 +121,7 @@
     """Generate the test description for the signature check."""
     self.__LogGenerateDescription('android.tests.sigtest')
     package = tools.TestPackage('SignatureTest', 'android.tests.sigtest')
+    package.AddAttribute('appNameSpace', 'android.tests.sigtest')
     package.AddAttribute('signatureCheck', 'true')
     package.AddAttribute('runner', '.InstrumentationRunner')
     package.AddTest('android.tests.sigtest.SignatureTest.signatureTest')
@@ -132,6 +133,7 @@
     """Generate the test description for the reference app tests."""
     self.__LogGenerateDescription('android.apidemos.cts')
     package = tools.TestPackage('ApiDemosReferenceTest', 'android.apidemos.cts')
+    package.AddAttribute('appNameSpace', 'android.apidemos.cts')
     package.AddAttribute('packageToTest', 'com.example.android.apis')
     package.AddAttribute('apkToTestName', 'ApiDemos')
     package.AddAttribute('runner', 'android.test.InstrumentationTestRunner')