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> </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')