aboutsummaryrefslogtreecommitdiffstats
path: root/sdkmanager
diff options
context:
space:
mode:
authorRaphael Moll <ralf@android.com>2012-04-17 14:17:40 -0700
committerRaphael Moll <ralf@android.com>2012-04-23 20:34:24 -0700
commit92529f27adec9ce5ec1cb3dfd79a15df7773e27c (patch)
tree2098a3fc203709f1fb4b63c712932d697a498fa6 /sdkmanager
parent7892606914a8ccbc349ff37f2b4e142d2ff84ad4 (diff)
downloadsdk-92529f27adec9ce5ec1cb3dfd79a15df7773e27c.zip
sdk-92529f27adec9ce5ec1cb3dfd79a15df7773e27c.tar.gz
sdk-92529f27adec9ce5ec1cb3dfd79a15df7773e27c.tar.bz2
SDK: XML to export platform stats.
This would be used later by the NPW. Change-Id: Iec6f24182f234d83217d83f0087aa92eabb7609d
Diffstat (limited to 'sdkmanager')
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java589
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java91
-rwxr-xr-xsdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd96
-rw-r--r--sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java73
-rwxr-xr-xsdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java172
-rwxr-xr-xsdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml48
6 files changed, 1069 insertions, 0 deletions
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java
new file mode 100755
index 0000000..4958505
--- /dev/null
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SdkStats.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright (C) 2012 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.sdklib.internal.repository;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.annotations.VisibleForTesting.Visibility;
+import com.android.sdklib.repository.SdkStatsConstants;
+import com.android.sdklib.util.SparseArray;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.UnknownHostException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.net.ssl.SSLKeyException;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+
+
+/**
+ * Retrieves stats on platforms.
+ * <p/>
+ * This returns information stored on the repository in a different XML file
+ * and isn't directly tied to the existence of the listed platforms.
+ */
+public class SdkStats {
+
+ public static class PlatformStatBase {
+ private final int mApiLevel;
+ private final String mVersionName;
+ private final String mCodeName;
+ private final float mShare;
+
+ public PlatformStatBase(int apiLevel,
+ String versionName,
+ String codeName,
+ float share) {
+ mApiLevel = apiLevel;
+ mVersionName = versionName;
+ mCodeName = codeName;
+ mShare = share;
+ }
+
+ /** The Android API Level for the platform. An int > 0. */
+ public int getApiLevel() {
+ return mApiLevel;
+ }
+
+ /** The official codename for this platform, for example "Cupcake". */
+ public String getCodeName() {
+ return mCodeName;
+ }
+
+ /** The official version name of this platform, for example "Android 1.5". */
+ public String getVersionName() {
+ return mVersionName;
+ }
+
+ /** An approximate share percentage of this platform and all the
+ * platforms of lower API level. */
+ public float getShare() {
+ return mShare;
+ }
+
+ /** Returns a string representation of this object, for debugging purposes. */
+ @Override
+ public String toString() {
+ return String.format("api=%d, code=%s, vers=%s, share=%.1f%%", //$NON-NLS-1$
+ mApiLevel, mCodeName, mVersionName, mShare);
+ }
+ }
+
+ public static class PlatformStat extends PlatformStatBase {
+ private final float mAccumShare;
+
+ public PlatformStat(int apiLevel,
+ String versionName,
+ String codeName,
+ float share,
+ float accumShare) {
+ super(apiLevel, versionName, codeName, share);
+ mAccumShare = accumShare;
+ }
+
+ public PlatformStat(PlatformStatBase base, float accumShare) {
+ super(base.getApiLevel(),
+ base.getVersionName(),
+ base.getCodeName(),
+ base.getShare());
+ mAccumShare = accumShare;
+ }
+
+ /** The accumulated approximate share percentage of that platform. */
+ public float getAccumShare() {
+ return mAccumShare;
+ }
+
+ /** Returns a string representation of this object, for debugging purposes. */
+ @Override
+ public String toString() {
+ return String.format("<Stat %s, accum=%.1f%%>", super.toString(), mAccumShare);
+ }
+ }
+
+ private final SparseArray<PlatformStat> mStats = new SparseArray<SdkStats.PlatformStat>();
+
+ public SdkStats() {
+ }
+
+ public SparseArray<PlatformStat> getStats() {
+ return mStats;
+ }
+
+ public void load(DownloadCache cache, boolean forceHttp, ITaskMonitor monitor) {
+
+ String url = SdkStatsConstants.URL_STATS;
+
+ if (forceHttp) {
+ url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ monitor.setProgressMax(5);
+ monitor.setDescription("Fetching %1$s", url);
+ monitor.incProgress(1);
+
+ Exception[] exception = new Exception[] { null };
+ Boolean[] validatorFound = new Boolean[] { Boolean.FALSE };
+ String[] validationError = new String[] { null };
+ Document validatedDoc = null;
+ String validatedUri = null;
+
+ InputStream xml = fetchUrl(url, cache, monitor.createSubMonitor(1), exception);
+
+ if (xml != null) {
+ monitor.setDescription("Validate XML");
+
+ // Explore the XML to find the potential XML schema version
+ int version = getXmlSchemaVersion(xml);
+
+ if (version >= 1 && version <= SdkStatsConstants.NS_LATEST_VERSION) {
+ // This should be a version we can handle. Try to validate it
+ // and report any error as invalid XML syntax,
+
+ String uri = validateXml(xml, url, version, validationError, validatorFound);
+ if (uri != null) {
+ // Validation was successful
+ validatedDoc = getDocument(xml, monitor);
+ validatedUri = uri;
+
+ }
+ } else if (version > SdkStatsConstants.NS_LATEST_VERSION) {
+ // The schema used is more recent than what is supported by this tool.
+ // We don't have an upgrade-path support yet, so simply ignore the document.
+ return;
+ }
+ }
+
+ // If any exception was handled during the URL fetch, display it now.
+ if (exception[0] != null) {
+ String reason = null;
+ if (exception[0] instanceof FileNotFoundException) {
+ // FNF has no useful getMessage, so we need to special handle it.
+ reason = "File not found";
+ } else if (exception[0] instanceof UnknownHostException &&
+ exception[0].getMessage() != null) {
+ // This has no useful getMessage yet could really use one
+ reason = String.format("Unknown Host %1$s", exception[0].getMessage());
+ } else if (exception[0] instanceof SSLKeyException) {
+ // That's a common error and we have a pref for it.
+ reason = "HTTPS SSL error. You might want to force download through HTTP in the settings.";
+ } else if (exception[0].getMessage() != null) {
+ reason = exception[0].getMessage();
+ } else {
+ // We don't know what's wrong. Let's give the exception class at least.
+ reason = String.format("Unknown (%1$s)", exception[0].getClass().getName());
+ }
+
+ monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason);
+ }
+
+ if (validationError[0] != null) {
+ monitor.logError("%s", validationError[0]); //$NON-NLS-1$
+ }
+
+ // Stop here if we failed to validate the XML. We don't want to load it.
+ if (validatedDoc == null) {
+ return;
+ }
+
+ monitor.incProgress(1);
+
+ if (xml != null) {
+ monitor.setDescription("Parse XML");
+ monitor.incProgress(1);
+ parseStatsDocument(validatedDoc, validatedUri, monitor);
+ }
+
+ // done
+ monitor.incProgress(1);
+ }
+
+ /**
+ * Fetches the document at the given URL and returns it as a stream. Returns
+ * null if anything wrong happens. References: <br/>
+ * URL Connection:
+ *
+ * @param urlString The URL to load, as a string.
+ * @param monitor {@link ITaskMonitor} related to this URL.
+ * @param outException If non null, where to store any exception that
+ * happens during the fetch.
+ * @see UrlOpener UrlOpener, which handles all URL logic.
+ */
+ private InputStream fetchUrl(String urlString,
+ DownloadCache cache,
+ ITaskMonitor monitor,
+ Exception[] outException) {
+ try {
+ return cache.openCachedUrl(urlString, monitor);
+ } catch (Exception e) {
+ if (outException != null) {
+ outException[0] = e;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Manually parses the root element of the XML to extract the schema version
+ * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N"
+ * declaration.
+ *
+ * @return 1..{@link SdkStatsConstants#NS_LATEST_VERSION} for a valid schema version
+ * or 0 if no schema could be found.
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected int getXmlSchemaVersion(InputStream xml) {
+ if (xml == null) {
+ return 0;
+ }
+
+ // Get an XML document
+ Document doc = null;
+ try {
+ xml.reset();
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setIgnoringComments(false);
+ factory.setValidating(false);
+
+ // Parse the old document using a non namespace aware builder
+ factory.setNamespaceAware(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+
+ // We don't want the default handler which prints errors to stderr.
+ builder.setErrorHandler(new ErrorHandler() {
+ @Override
+ public void warning(SAXParseException e) throws SAXException {
+ // pass
+ }
+ @Override
+ public void fatalError(SAXParseException e) throws SAXException {
+ throw e;
+ }
+ @Override
+ public void error(SAXParseException e) throws SAXException {
+ throw e;
+ }
+ });
+
+ doc = builder.parse(xml);
+
+ // Prepare a new document using a namespace aware builder
+ factory.setNamespaceAware(true);
+ builder = factory.newDocumentBuilder();
+
+ } catch (Exception e) {
+ // Failed to reset XML stream
+ // Failed to get builder factor
+ // Failed to create XML document builder
+ // Failed to parse XML document
+ // Failed to read XML document
+ }
+
+ if (doc == null) {
+ return 0;
+ }
+
+ // Check the root element is an XML with at least the following properties:
+ // <sdk:sdk-addons-list
+ // xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N">
+ //
+ // Note that we don't have namespace support enabled, we just do it manually.
+
+ Pattern nsPattern = Pattern.compile(SdkStatsConstants.NS_PATTERN);
+
+ String prefix = null;
+ for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) {
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ prefix = null;
+ String name = child.getNodeName();
+ int pos = name.indexOf(':');
+ if (pos > 0 && pos < name.length() - 1) {
+ prefix = name.substring(0, pos);
+ name = name.substring(pos + 1);
+ }
+ if (SdkStatsConstants.NODE_SDK_STATS.equals(name)) {
+ NamedNodeMap attrs = child.getAttributes();
+ String xmlns = "xmlns"; //$NON-NLS-1$
+ if (prefix != null) {
+ xmlns += ":" + prefix; //$NON-NLS-1$
+ }
+ Node attr = attrs.getNamedItem(xmlns);
+ if (attr != null) {
+ String uri = attr.getNodeValue();
+ if (uri != null) {
+ Matcher m = nsPattern.matcher(uri);
+ if (m.matches()) {
+ String version = m.group(1);
+ try {
+ return Integer.parseInt(version);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Validates this XML against one of the requested SDK Repository schemas.
+ * If the XML was correctly validated, returns the schema that worked.
+ * If it doesn't validate, returns null and stores the error in outError[0].
+ * If we can't find a validator, returns null and set validatorFound[0] to false.
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected String validateXml(InputStream xml, String url, int version,
+ String[] outError, Boolean[] validatorFound) {
+
+ if (xml == null) {
+ return null;
+ }
+
+ try {
+ Validator validator = getValidator(version);
+
+ if (validator == null) {
+ validatorFound[0] = Boolean.FALSE;
+ outError[0] = String.format(
+ "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.",
+ url);
+ return null;
+ }
+
+ validatorFound[0] = Boolean.TRUE;
+
+ // Reset the stream if it supports that operation.
+ xml.reset();
+
+ // Validation throws a bunch of possible Exceptions on failure.
+ validator.validate(new StreamSource(xml));
+ return SdkStatsConstants.getSchemaUri(version);
+
+ } catch (SAXParseException e) {
+ outError[0] = String.format(
+ "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s",
+ url,
+ e.getLineNumber(),
+ e.getColumnNumber(),
+ e.toString());
+
+ } catch (Exception e) {
+ outError[0] = String.format(
+ "XML verification failed for %1$s.\nError: %2$s",
+ url,
+ e.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Helper method that returns a validator for our XSD, or null if the current Java
+ * implementation can't process XSD schemas.
+ *
+ * @param version The version of the XML Schema.
+ * See {@link SdkStatsConstants#getXsdStream(int)}
+ */
+ private Validator getValidator(int version) throws SAXException {
+ InputStream xsdStream = SdkStatsConstants.getXsdStream(version);
+ SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+ if (factory == null) {
+ return null;
+ }
+
+ // This may throw a SAX Exception if the schema itself is not a valid XSD
+ Schema schema = factory.newSchema(new StreamSource(xsdStream));
+
+ Validator validator = schema == null ? null : schema.newValidator();
+
+ return validator;
+ }
+
+ /**
+ * Takes an XML document as a string as parameter and returns a DOM for it.
+ *
+ * On error, returns null and prints a (hopefully) useful message on the monitor.
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected Document getDocument(InputStream xml, ITaskMonitor monitor) {
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setIgnoringComments(true);
+ factory.setNamespaceAware(true);
+
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ xml.reset();
+ Document doc = builder.parse(new InputSource(xml));
+
+ return doc;
+ } catch (ParserConfigurationException e) {
+ monitor.logError("Failed to create XML document builder");
+
+ } catch (SAXException e) {
+ monitor.logError("Failed to parse XML document");
+
+ } catch (IOException e) {
+ monitor.logError("Failed to read XML document");
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses all valid platforms found in the XML.
+ * Changes the stats array returned by {@link #getStats()}
+ * (also returns the value directly, useful for unti tests.)
+ */
+ @VisibleForTesting(visibility=Visibility.PRIVATE)
+ protected SparseArray<PlatformStat> parseStatsDocument(
+ Document doc,
+ String nsUri,
+ ITaskMonitor monitor) {
+
+ String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$
+ if (baseUrl != null) {
+ if (baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$
+ baseUrl = null;
+ }
+ }
+
+ SparseArray<PlatformStatBase> platforms = new SparseArray<SdkStats.PlatformStatBase>();
+ int maxApi = 0;
+
+ Node root = getFirstChild(doc, nsUri, SdkStatsConstants.NODE_SDK_STATS);
+ if (root != null) {
+ for (Node child = root.getFirstChild();
+ child != null;
+ child = child.getNextSibling()) {
+ if (child.getNodeType() == Node.ELEMENT_NODE &&
+ nsUri.equals(child.getNamespaceURI()) &&
+ child.getLocalName().equals(SdkStatsConstants.NODE_PLATFORM)) {
+
+ try {
+ Node node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_API_LEVEL);
+ int apiLevel = Integer.parseInt(node.getTextContent().trim());
+
+ if (apiLevel < 1) {
+ // bad API level, ignore it.
+ continue;
+ }
+
+ if (platforms.indexOfKey(apiLevel) >= 0) {
+ // if we already loaded that API, ignore duplicates
+ continue;
+ }
+
+ String codeName =
+ getFirstChild(child, nsUri, SdkStatsConstants.NODE_CODENAME).
+ getTextContent().trim();
+ String versName =
+ getFirstChild(child, nsUri, SdkStatsConstants.NODE_VERSION).
+ getTextContent().trim();
+
+ if (codeName == null || versName == null ||
+ codeName.length() == 0 || versName.length() == 0) {
+ // bad names. ignore.
+ continue;
+ }
+
+ node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_SHARE);
+ float percent = Float.parseFloat(node.getTextContent().trim());
+
+ if (percent < 0 || percent > 100) {
+ // invalid percentage. ignore.
+ continue;
+ }
+
+ PlatformStatBase p = new PlatformStatBase(
+ apiLevel, versName, codeName, percent);
+ platforms.put(apiLevel, p);
+
+ maxApi = apiLevel > maxApi ? apiLevel : maxApi;
+
+ } catch (Exception ignore) {
+ // Error parsing this platform. Ignore it.
+ continue;
+ }
+ }
+ }
+ }
+
+ mStats.clear();
+
+ // Compute cumulative share percents & fill in final map
+ for (int api = 1; api <= maxApi; api++) {
+ PlatformStatBase p = platforms.get(api);
+ if (p == null) {
+ continue;
+ }
+
+ float sum = p.getShare();
+ for (int j = api + 1; j <= maxApi; j++) {
+ PlatformStatBase pj = platforms.get(j);
+ if (pj != null) {
+ sum += pj.getShare();
+ }
+ }
+
+ mStats.put(api, new PlatformStat(p, sum));
+ }
+
+ return mStats;
+ }
+
+ /**
+ * Returns the first child element with the given XML local name.
+ * If xmlLocalName is null, returns the very first child element.
+ */
+ private Node getFirstChild(Node node, String nsUri, String xmlLocalName) {
+
+ for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
+ if (child.getNodeType() == Node.ELEMENT_NODE &&
+ nsUri.equals(child.getNamespaceURI())) {
+ if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) {
+ return child;
+ }
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java
new file mode 100755
index 0000000..06f8d39
--- /dev/null
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/SdkStatsConstants.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.sdklib.repository;
+
+
+import java.io.InputStream;
+
+/**
+ * Public constants for the sdk-stats XML Schema.
+ */
+public class SdkStatsConstants {
+
+ /** The canonical URL filename for addons-list XML files. */
+ public static final String URL_DEFAULT_FILENAME = "stats-1.xml"; //$NON-NLS-1$
+
+ /** The URL where to find the official addons list fle. */
+ public static final String URL_STATS =
+ SdkRepoConstants.URL_GOOGLE_SDK_SITE + URL_DEFAULT_FILENAME;
+
+ /** The base of our sdk-addons-list XML namespace. */
+ private static final String NS_BASE =
+ "http://schemas.android.com/sdk/android/stats/"; //$NON-NLS-1$
+
+ /**
+ * The pattern of our sdk-stats XML namespace.
+ * Matcher's group(1) is the schema version (integer).
+ */
+ public static final String NS_PATTERN = NS_BASE + "([1-9][0-9]*)"; //$NON-NLS-1$
+
+ /** The latest version of the sdk-stats XML Schema.
+ * Valid version numbers are between 1 and this number, included. */
+ public static final int NS_LATEST_VERSION = 1;
+
+ /** The XML namespace of the latest sdk-stats XML. */
+ public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION);
+
+ /** The root sdk-stats element */
+ public static final String NODE_SDK_STATS = "sdk-stats"; //$NON-NLS-1$
+
+ /** A platform stat. */
+ public static final String NODE_PLATFORM = "platform"; //$NON-NLS-1$
+
+ /** The Android API Level for the platform. An int > 0. */
+ public static final String NODE_API_LEVEL = "api-level"; //$NON-NLS-1$
+
+ /** The official codename for this platform, for example "Cupcake". */
+ public static final String NODE_CODENAME = "codename"; //$NON-NLS-1$
+
+ /** The official version name of this platform, for example "Android 1.5". */
+ public static final String NODE_VERSION = "version"; //$NON-NLS-1$
+
+ /**
+ * The <em>approximate</em> share percentage of that platform.
+ * See the caveat in sdk-stats-1.xsd about value freshness and accuracy.
+ */
+ public static final String NODE_SHARE = "share"; //$NON-NLS-1$
+
+ /**
+ * Returns a stream to the requested sdk-stats XML Schema.
+ *
+ * @param version Between 1 and {@link #NS_LATEST_VERSION}, included.
+ * @return An {@link InputStream} object for the local XSD file or
+ * null if there is no schema for the requested version.
+ */
+ public static InputStream getXsdStream(int version) {
+ String filename = String.format("sdk-stats-%d.xsd", version); //$NON-NLS-1$
+ return SdkStatsConstants.class.getResourceAsStream(filename);
+ }
+
+ /**
+ * Returns the URI of the sdk-stats schema for the given version number.
+ * @param version Between 1 and {@link #NS_LATEST_VERSION} included.
+ */
+ public static String getSchemaUri(int version) {
+ return String.format(NS_BASE + "%d", version); //$NON-NLS-1$
+ }
+}
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd
new file mode 100755
index 0000000..2944b12
--- /dev/null
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/repository/sdk-stats-1.xsd
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Copyright (C) 2012 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.
+-->
+<xsd:schema
+ targetNamespace="http://schemas.android.com/sdk/android/stats/1"
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+ xmlns:ast="http://schemas.android.com/sdk/android/stats/1"
+ elementFormDefault="qualified"
+ attributeFormDefault="unqualified"
+ version="1">
+
+ <!--
+ A simple list of platforms provided by the SDK Manager and some
+ statistics on them, namely the market share percentage for that
+ platform.
+ This can be used by the SDK Manager or the ADT New Project Wizard
+ to give users an idea of the relative install base of platforms.
+
+ Scope, Caveat & Limitation:
+ The "share percentage" corresponds to the Platform Versions table
+ from the SDK Dashboard as seen at
+ http://developer.android.com/resources/dashboard/platform-versions.html
+ However the data is not automatically generated and there is NO
+ freshness implied. The values may or may not be up-to-date and it is
+ most likely they will only get refreshed when there's a significant
+ change that affects the usage of the SDK tools.
+
+ => The data is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ OR CONDITIONS OF ANY KIND, either express or implied.
+ -->
+
+ <xsd:element name="sdk-stats" type="ast:platformsListType" />
+
+ <xsd:complexType name="platformsListType">
+ <xsd:annotation>
+ <xsd:documentation>
+ A simple list of platform stats.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:choice minOccurs="0" maxOccurs="unbounded">
+ <xsd:element name="platform" type="ast:platformType" />
+ </xsd:choice>
+ </xsd:complexType>
+
+
+ <!-- The definition of stats for a platform. -->
+
+ <xsd:complexType name="platformType">
+ <xsd:annotation>
+ <xsd:documentation>Stats information for a given Android platform.
+ The api-level acts as a key, and it is epxected there should only
+ be one platform listed with the same API-level.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:all>
+ <!-- The Android API Level for the platform. An int > 0. -->
+ <xsd:element name="api-level" type="xsd:positiveInteger" />
+
+ <!-- The official codename for this platform, for example "Cupcake". -->
+ <xsd:element name="codename" type="xsd:normalizedString" />
+
+ <!-- The official version name of this platform, for example "Android 1.5". -->
+ <xsd:element name="version" type="xsd:normalizedString" />
+
+ <!-- An approximate share percentage of this platform. -->
+ <xsd:element name="share" type="ast:percent" />
+ </xsd:all>
+ </xsd:complexType>
+
+
+ <!-- A decimal percentage, between 0.0 and 100.0% -->
+
+ <xsd:simpleType name="percent" id="percent">
+ <xsd:annotation>
+ <xsd:documentation>A decimal percentage, between 0.0 and 100.0%.</xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:decimal">
+ <xsd:minInclusive value="0"/>
+ <xsd:maxInclusive value="100"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+</xsd:schema>
diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java
index b948e8c..7c535a8 100644
--- a/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java
+++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/util/SparseArray.java
@@ -303,6 +303,79 @@ public class SparseArray<E> {
mSize = pos + 1;
}
+ public SparseArray<E> getUnmodifiable() {
+ final SparseArray<E> mStorage = this;
+ return new SparseArray<E>() {
+
+ @Override
+ public E get(int key) {
+ return mStorage.get(key);
+ }
+
+ @Override
+ public E get(int key, E valueIfKeyNotFound) {
+ return mStorage.get(key, valueIfKeyNotFound);
+ }
+
+ @Override
+ public void delete(int key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void remove(int key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void put(int key, E value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ return mStorage.size();
+ }
+
+ @Override
+ public int keyAt(int index) {
+ return mStorage.keyAt(index);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public E valueAt(int index) {
+ return mStorage.valueAt(index);
+ }
+
+ @Override
+ public void setValueAt(int index, E value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int indexOfKey(int key) {
+ return mStorage.indexOfKey(key);
+ }
+
+ @Override
+ public int indexOfValue(E value) {
+ return mStorage.indexOfValue(value);
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void append(int key, E value) {
+ throw new UnsupportedOperationException();
+ }
+
+ };
+ }
+
private static int binarySearch(int[] a, int start, int len, int key) {
int high = start + len, low = start - 1, guess;
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java
new file mode 100755
index 0000000..bc2a181
--- /dev/null
+++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/internal/repository/SdkStatsTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.sdklib.internal.repository;
+
+import com.android.sdklib.internal.repository.SdkStats.PlatformStat;
+import com.android.sdklib.repository.SdkStatsConstants;
+import com.android.sdklib.util.SparseArray;
+
+import org.w3c.dom.Document;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link SdkStats}
+ */
+public class SdkStatsTest extends TestCase {
+
+ /**
+ * An internal helper class to give us visibility to the protected members we want
+ * to test.
+ */
+ private static class MockSdkStats extends SdkStats {
+
+ public SparseArray<PlatformStat> _parseStatsDocument(Document doc,
+ String nsUri,
+ ITaskMonitor monitor) {
+ return super.parseStatsDocument(doc, nsUri, monitor);
+ }
+
+ public int _getXmlSchemaVersion(InputStream xml) {
+ return super.getXmlSchemaVersion(xml);
+ }
+
+ public String _validateXml(InputStream xml, String url, int version,
+ String[] outError, Boolean[] validatorFound) {
+ return super.validateXml(xml, url, version, outError, validatorFound);
+ }
+
+ public Document _getDocument(InputStream xml, ITaskMonitor monitor) {
+ return super.getDocument(xml, monitor);
+ }
+
+ }
+
+ private MockSdkStats mStats;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mStats = new MockSdkStats();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ mStats = null;
+ }
+
+ /**
+ * Validate we can still load a valid addon schema version 1
+ */
+ public void testLoadSample_1() throws Exception {
+ InputStream xmlStream =
+ getTestResource("/com/android/sdklib/testdata/stats_sample_1.xml");
+
+ // guess the version from the XML document
+ int version = mStats._getXmlSchemaVersion(xmlStream);
+ assertEquals(1, version);
+
+ Boolean[] validatorFound = new Boolean[] { Boolean.FALSE };
+ String[] validationError = new String[] { null };
+ String url = "not-a-valid-url://stats.xml";
+
+ String uri = mStats._validateXml(
+ xmlStream, url, version, validationError, validatorFound);
+ assertEquals(Boolean.TRUE, validatorFound[0]);
+ assertEquals(null, validationError[0]);
+ assertEquals(SdkStatsConstants.getSchemaUri(1), uri);
+
+ // Validation was successful, load the document
+ MockMonitor monitor = new MockMonitor();
+ Document doc = mStats._getDocument(xmlStream, monitor);
+ assertNotNull(doc);
+
+ // Get the sites
+ SparseArray<PlatformStat> result = mStats._parseStatsDocument(doc, uri, monitor);
+
+ assertEquals("", monitor.getCapturedDescriptions());
+ assertEquals("", monitor.getCapturedLog());
+ assertEquals("", monitor.getCapturedErrorLog());
+ assertEquals("", monitor.getCapturedVerboseLog());
+
+ // check what we found
+ assertEquals(3, result.size());
+ int len = result.size();
+
+ int[] keys = new int[len];
+ PlatformStat[] stats = new PlatformStat[len];
+ for (int i = 0; i < len; i++) {
+ keys[i] = result.keyAt(i);
+ stats[i] = result.valueAt(i);
+ }
+
+ assertEquals(
+ "[3, 5, 42]",
+ Arrays.toString(keys));
+
+ assertEquals(
+ "[<Stat api=3, code=Vanilla, vers=Android 0.5, share=0.1%, accum=100.0%>, " +
+ "<Stat api=5, code=Coffee, vers=Android 42.0, share=25.8%, accum=99.9%>, " +
+ "<Stat api=42, code=Chocolate, vers=Android 32.64, share=74.1%, accum=74.1%>]",
+ Arrays.toString(stats));
+ }
+
+ /**
+ * Returns an SdkLib file resource as a {@link ByteArrayInputStream},
+ * which has the advantage that we can use {@link InputStream#reset()} on it
+ * at any time to read it multiple times.
+ * <p/>
+ * The default for getResourceAsStream() is to return a {@link FileInputStream} that
+ * does not support reset(), yet we need it in the tested code.
+ *
+ * @throws IOException if some I/O read fails
+ */
+ private ByteArrayInputStream getTestResource(String filename) throws IOException {
+ InputStream xmlStream = this.getClass().getResourceAsStream(filename);
+
+ try {
+ byte[] data = new byte[8192];
+ int offset = 0;
+ int n;
+
+ while ((n = xmlStream.read(data, offset, data.length - offset)) != -1) {
+ offset += n;
+
+ if (offset == data.length) {
+ byte[] newData = new byte[offset + 8192];
+ System.arraycopy(data, 0, newData, 0, offset);
+ data = newData;
+ }
+ }
+
+ return new ByteArrayInputStream(data, 0, offset);
+ } finally {
+ if (xmlStream != null) {
+ xmlStream.close();
+ }
+ }
+ }
+}
diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml
new file mode 100755
index 0000000..344b8a5
--- /dev/null
+++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/testdata/stats_sample_1.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Copyright (C) 2012 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.
+-->
+<sdk:sdk-stats
+ xmlns:sdk="http://schemas.android.com/sdk/android/stats/1">
+
+ <sdk:platform>
+ <sdk:api-level>3</sdk:api-level>
+ <sdk:codename>Vanilla</sdk:codename>
+ <sdk:version>Android 0.5</sdk:version>
+ <sdk:share>0.1</sdk:share>
+ </sdk:platform>
+
+ <sdk:platform>
+ <sdk:api-level>5</sdk:api-level>
+ <sdk:codename>Coffee</sdk:codename>
+ <sdk:version>Android 42.0</sdk:version>
+ <sdk:share>25.8</sdk:share>
+ </sdk:platform>
+
+ <sdk:platform>
+ <sdk:api-level>42</sdk:api-level>
+ <sdk:codename>Chocolate</sdk:codename>
+ <sdk:version>Android 32.64</sdk:version>
+ <sdk:share>74.1</sdk:share>
+ </sdk:platform>
+
+ <sdk:platform>
+ <sdk:api-level>5</sdk:api-level>
+ <sdk:codename>Ignored</sdk:codename>
+ <sdk:version>API 5 was already seen above</sdk:version>
+ <sdk:share>99.9</sdk:share>
+ </sdk:platform>
+
+</sdk:sdk-stats>