diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java b/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java
index 9d4794c08..2fbfb3485 100644
--- a/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java
+++ b/samples/XmlAdapters/src/com/example/android/xmladapters/Adapters.java
@@ -16,9 +16,6 @@
package com.example.android.xmladapters;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
@@ -37,6 +34,9 @@ import android.widget.ImageView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -257,7 +257,6 @@ import java.util.HashMap;
* attr ref android.R.styleable#CursorAdapter_TransformItem_withClass
* attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression
*/
-@SuppressWarnings({"JavadocReference"})
public class Adapters {
private static final String ADAPTER_CURSOR = "cursor-adapter";
@@ -898,10 +897,13 @@ public class Adapters {
* of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders.
*/
private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter {
+ private Context mContext;
private String mUri;
private final String mSelection;
private final String[] mSelectionArgs;
private final String mSortOrder;
+ private final int[] mTo;
+ private final String[] mFrom;
private final String[] mColumns;
private final CursorBinder[] mBinders;
private AsyncTask mLoadTask;
@@ -913,6 +915,8 @@ public class Adapters {
super(context, layout, null, from, to);
mContext = context;
mUri = uri;
+ mFrom = from;
+ mTo = to;
mSelection = selection;
mSelectionArgs = selectionArgs;
mSortOrder = sortOrder;
@@ -935,14 +939,14 @@ public class Adapters {
@Override
public void bindView(View view, Context context, Cursor cursor) {
final int count = mTo.length;
- final int[] from = mFrom;
final int[] to = mTo;
final CursorBinder[] binders = mBinders;
for (int i = 0; i < count; i++) {
final View v = view.findViewById(to[i]);
if (v != null) {
- binders[i].bind(v, cursor, from[i]);
+ // Not optimal, the column index could be cached
+ binders[i].bind(v, cursor, cursor.getColumnIndex(mFrom[i]));
}
}
}
diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java b/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java
index c84f9d528..eb91fcbb9 100644
--- a/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java
+++ b/samples/XmlAdapters/src/com/example/android/xmladapters/ImageDownloader.java
@@ -12,7 +12,9 @@
* 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.example.android.xmladapters;
+ */
+
+package com.example.android.xmladapters;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
@@ -58,14 +60,16 @@ public class ImageDownloader {
// Hard cache, with a fixed maximum capacity and a life duration
private final HashMap sHardBitmapCache =
new LinkedHashMap(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
+ private static final long serialVersionUID = -7190622541619388252L;
@Override
protected boolean removeEldestEntry(LinkedHashMap.Entry eldest) {
if (size() > HARD_CACHE_CAPACITY) {
// Entries push-out of hard reference cache are transferred to soft reference cache
sSoftBitmapCache.put(eldest.getKey(), new SoftReference(eldest.getValue()));
return true;
- } else
+ } else {
return false;
+ }
}
};
diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java b/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java
index 16df24654..defdc19c4 100644
--- a/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java
+++ b/samples/XmlAdapters/src/com/example/android/xmladapters/RssReaderActivity.java
@@ -17,7 +17,6 @@
package com.example.android.xmladapters;
import android.app.ListActivity;
-import android.content.XmlDocumentProvider;
import android.net.Uri;
import android.os.Bundle;
import android.widget.AdapterView.OnItemClickListener;
diff --git a/samples/XmlAdapters/src/com/example/android/xmladapters/XmlDocumentProvider.java b/samples/XmlAdapters/src/com/example/android/xmladapters/XmlDocumentProvider.java
new file mode 100644
index 000000000..d0ab0cfe7
--- /dev/null
+++ b/samples/XmlAdapters/src/com/example/android/xmladapters/XmlDocumentProvider.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2011 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.example.android.xmladapters;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpGet;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.CursorAdapter;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ *
+ * A read-only content provider which extracts data out of an XML document.
+ *
+ * A XPath-like selection pattern is used to select some nodes in the XML document. Each such
+ * node will create a row in the {@link Cursor} result.
+ *
+ * Each row is then populated with columns that are also defined as XPath-like projections. These
+ * projections fetch attributes values or text in the matching row node or its children.
+ *
+ * To add this provider in your application, you should add its declaration to your application
+ * manifest:
+ *
+ * <provider android:name="android.content.XmlDocumentProvider" android:authorities="xmldocument" />
+ *
+ *
+ *
+ * Node selection syntax
+ * The node selection syntax is made of the concatenation of an arbitrary number (at least one) of
+ * /node_name node selection patterns.
+ *
+ * The /root/child1/child2 pattern will for instance match all nodes named
+ * child2 which are children of a node named child1 which are themselves
+ * children of a root node named root.
+ *
+ * Any / separator in the previous expression can be replaced by a //
+ * separator instead, which indicated a descendant instead of a child.
+ *
+ * The //node1//node2 pattern will for instance match all nodes named
+ * node2 which are descendant of a node named node1 located anywhere in
+ * the document hierarchy.
+ *
+ * Node names can contain namespaces in the form namespace:node.
+ *
+ * Projection syntax
+ * For every selected node, the projection will then extract actual data from this node and its
+ * descendant.
+ *
+ * Use a syntax similar to the selection syntax described above to select the text associated
+ * with a child of the selected node. The implicit root of this projection pattern is the selected
+ * node. / will hence refer to the text of the selected node, while
+ * /child1 will fetch the text of its child named child1 and
+ * //child1 will match any descendant named child1. If several
+ * nodes match the projection pattern, their texts are appended as a result.
+ *
+ * A projection can also fetch any node attribute by appending a @attribute_name
+ * pattern to the previously described syntax. //child1@price will for instance match
+ * the attribute price of any child1 descendant.
+ *
+ * If a projection does not match any node/attribute, its associated value will be an empty
+ * string.
+ *
+ * Example
+ * Using the following XML document:
+ *
+ * <library>
+ * <book id="EH94">
+ * <title>The Old Man and the Sea</title>
+ * <author>Ernest Hemingway</author>
+ * </book>
+ * <book id="XX10">
+ * <title>The Arabian Nights: Tales of 1,001 Nights</title>
+ * </book>
+ * <no-id>
+ * <book>
+ * <title>Animal Farm</title>
+ * <author>George Orwell</author>
+ * </book>
+ * </no-id>
+ * </library>
+ *
+ * A selection pattern of /library//book will match the three book entries (while
+ * /library/book will only match the first two ones).
+ *
+ * Defining the projections as /title, /author and @id
+ * will retrieve the associated data. Note that the author of the second book as well as the id of
+ * the third are empty strings.
+ */
+public class XmlDocumentProvider extends ContentProvider {
+ /*
+ * Ideas for improvement:
+ * - Expand XPath-like syntax to allow for [nb] child number selector
+ * - Address the starting . bug in AbstractCursor which prevents a true XPath syntax.
+ * - Provide an alternative to concatenation when several node match (list-like).
+ * - Support namespaces in attribute names.
+ * - Incremental Cursor creation, pagination
+ */
+ private static final String LOG_TAG = "XmlDocumentProvider";
+ private AndroidHttpClient mHttpClient;
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ /**
+ * Query data from the XML document referenced in the URI.
+ *
+ *
The XML document can be a local resource or a file that will be downloaded from the
+ * Internet. In the latter case, your application needs to request the INTERNET permission in
+ * its manifest.
+ *
+ * The URI will be of the form content://xmldocument/?resource=R.xml.myFile for a
+ * local resource. xmldocument should match the authority declared for this
+ * provider in your manifest. Internet documents are referenced using
+ * content://xmldocument/?url= followed by an encoded version of the URL of your
+ * document (see {@link Uri#encode(String)}).
+ *
+ * The number of columns of the resulting Cursor is equal to the size of the projection
+ * array plus one, named _id which will contain a unique row id (allowing the
+ * Cursor to be used with a {@link CursorAdapter}). The other columns' names are the projection
+ * patterns.
+ *
+ * @param uri The URI of your local resource or Internet document.
+ * @param projection A set of patterns that will be used to extract data from each selected
+ * node. See class documentation for pattern syntax.
+ * @param selection A selection pattern which will select the nodes that will create the
+ * Cursor's rows. See class documentation for pattern syntax.
+ * @param selectionArgs This parameter is ignored.
+ * @param sortOrder The row order in the resulting cursor is determined from the node order in
+ * the XML document. This parameter is ignored.
+ * @return A Cursor or null in case of error.
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+
+ XmlPullParser parser = null;
+ mHttpClient = null;
+
+ final String url = uri.getQueryParameter("url");
+ if (url != null) {
+ parser = getUriXmlPullParser(url);
+ } else {
+ final String resource = uri.getQueryParameter("resource");
+ if (resource != null) {
+ Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+ getContext().getPackageName() + "/" + resource);
+ parser = getResourceXmlPullParser(resourceUri);
+ }
+ }
+
+ if (parser != null) {
+ XMLCursor xmlCursor = new XMLCursor(selection, projection);
+ try {
+ xmlCursor.parseWith(parser);
+ return xmlCursor;
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "I/O error while parsing XML " + uri, e);
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Error while parsing XML " + uri, e);
+ } finally {
+ if (mHttpClient != null) {
+ mHttpClient.close();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an XmlPullParser for the provided URL. Can be overloaded to provide your own parser.
+ * @param url The URL of the XML document that is to be parsed.
+ * @return An XmlPullParser on this document.
+ */
+ protected XmlPullParser getUriXmlPullParser(String url) {
+ XmlPullParser parser = null;
+ try {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ parser = factory.newPullParser();
+ } catch (XmlPullParserException e) {
+ Log.e(LOG_TAG, "Unable to create XmlPullParser", e);
+ return null;
+ }
+
+ InputStream inputStream = null;
+ try {
+ final HttpGet get = new HttpGet(url);
+ mHttpClient = AndroidHttpClient.newInstance("Android");
+ HttpResponse response = mHttpClient.execute(get);
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ final HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ inputStream = entity.getContent();
+ }
+ }
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Error while retrieving XML file " + url, e);
+ return null;
+ }
+
+ try {
+ parser.setInput(inputStream, null);
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Error while reading XML file from " + url, e);
+ return null;
+ }
+
+ return parser;
+ }
+
+ /**
+ * Creates an XmlPullParser for the provided local resource. Can be overloaded to provide your
+ * own parser.
+ * @param resourceUri A fully qualified resource name referencing a local XML resource.
+ * @return An XmlPullParser on this resource.
+ */
+ protected XmlPullParser getResourceXmlPullParser(Uri resourceUri) {
+ //OpenResourceIdResult resourceId;
+ try {
+ String authority = resourceUri.getAuthority();
+ Resources r;
+ if (TextUtils.isEmpty(authority)) {
+ throw new FileNotFoundException("No authority: " + resourceUri);
+ } else {
+ try {
+ r = getContext().getPackageManager().getResourcesForApplication(authority);
+ } catch (NameNotFoundException ex) {
+ throw new FileNotFoundException("No package found for authority: " + resourceUri);
+ }
+ }
+ List path = resourceUri.getPathSegments();
+ if (path == null) {
+ throw new FileNotFoundException("No path: " + resourceUri);
+ }
+ int len = path.size();
+ int id;
+ if (len == 1) {
+ try {
+ id = Integer.parseInt(path.get(0));
+ } catch (NumberFormatException e) {
+ throw new FileNotFoundException("Single path segment is not a resource ID: " + resourceUri);
+ }
+ } else if (len == 2) {
+ id = r.getIdentifier(path.get(1), path.get(0), authority);
+ } else {
+ throw new FileNotFoundException("More than two path segments: " + resourceUri);
+ }
+ if (id == 0) {
+ throw new FileNotFoundException("No resource found for: " + resourceUri);
+ }
+
+ return r.getXml(id);
+ } catch (FileNotFoundException e) {
+ Log.w(LOG_TAG, "XML resource not found: " + resourceUri.toString(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns "vnd.android.cursor.dir/xmldoc".
+ */
+ @Override
+ public String getType(Uri uri) {
+ return "vnd.android.cursor.dir/xmldoc";
+ }
+
+ /**
+ * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
+ **/
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
+ **/
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This ContentProvider is read-only. This method throws an UnsupportedOperationException.
+ **/
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static class XMLCursor extends MatrixCursor {
+ private final Pattern mSelectionPattern;
+ private Pattern[] mProjectionPatterns;
+ private String[] mAttributeNames;
+ private String[] mCurrentValues;
+ private BitSet[] mActiveTextDepthMask;
+ private final int mNumberOfProjections;
+
+ public XMLCursor(String selection, String[] projections) {
+ super(projections);
+ // The first column in projections is used for the _ID
+ mNumberOfProjections = projections.length - 1;
+ mSelectionPattern = createPattern(selection);
+ createProjectionPattern(projections);
+ }
+
+ private Pattern createPattern(String input) {
+ String pattern = input.replaceAll("//", "/(.*/|)").replaceAll("^/", "^/") + "$";
+ return Pattern.compile(pattern);
+ }
+
+ private void createProjectionPattern(String[] projections) {
+ mProjectionPatterns = new Pattern[mNumberOfProjections];
+ mAttributeNames = new String[mNumberOfProjections];
+ mActiveTextDepthMask = new BitSet[mNumberOfProjections];
+ // Add a column to store _ID
+ mCurrentValues = new String[mNumberOfProjections + 1];
+
+ for (int i=0; i= 0) {
+ mAttributeNames[i] = projection.substring(atIndex+1);
+ projection = projection.substring(0, atIndex);
+ } else {
+ mAttributeNames[i] = null;
+ }
+
+ // Conforms to XPath standard: reference to local context starts with a .
+ if (projection.charAt(0) == '.') {
+ projection = projection.substring(1);
+ }
+ mProjectionPatterns[i] = createPattern(projection);
+ }
+ }
+
+ public void parseWith(XmlPullParser parser) throws IOException, XmlPullParserException {
+ StringBuilder path = new StringBuilder();
+ Stack pathLengthStack = new Stack();
+
+ // There are two parsing mode: in root mode, rootPath is updated and nodes matching
+ // selectionPattern are searched for and currentNodeDepth is negative.
+ // When a node matching selectionPattern is found, currentNodeDepth is set to 0 and
+ // updated as children are parsed and projectionPatterns are searched in nodePath.
+ int currentNodeDepth = -1;
+
+ // Index where local selected node path starts from in path
+ int currentNodePathStartIndex = 0;
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ // Update path
+ pathLengthStack.push(path.length());
+ path.append('/');
+ String prefix = null;
+ try {
+ // getPrefix is not supported by local Xml resource parser
+ prefix = parser.getPrefix();
+ } catch (RuntimeException e) {
+ prefix = null;
+ }
+ if (prefix != null) {
+ path.append(prefix);
+ path.append(':');
+ }
+ path.append(parser.getName());
+
+ if (currentNodeDepth >= 0) {
+ currentNodeDepth++;
+ } else {
+ // A node matching selection is found: initialize child parsing mode
+ if (mSelectionPattern.matcher(path.toString()).matches()) {
+ currentNodeDepth = 0;
+ currentNodePathStartIndex = path.length();
+ mCurrentValues[0] = Integer.toString(getCount()); // _ID
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ // Reset values to default (empty string)
+ mCurrentValues[i + 1] = "";
+ mActiveTextDepthMask[i].clear();
+ }
+ }
+ }
+
+ // This test has to be separated from the previous one as currentNodeDepth can
+ // be modified above (when a node matching selection is found).
+ if (currentNodeDepth >= 0) {
+ final String localNodePath = path.substring(currentNodePathStartIndex);
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ if (mProjectionPatterns[i].matcher(localNodePath).matches()) {
+ String attribute = mAttributeNames[i];
+ if (attribute != null) {
+ mCurrentValues[i + 1] =
+ parser.getAttributeValue(null, attribute);
+ } else {
+ mActiveTextDepthMask[i].set(currentNodeDepth, true);
+ }
+ }
+ }
+ }
+
+ } else if (eventType == XmlPullParser.END_TAG) {
+ // Pop last node from path
+ final int length = pathLengthStack.pop();
+ path.setLength(length);
+
+ if (currentNodeDepth >= 0) {
+ if (currentNodeDepth == 0) {
+ // Leaving a selection matching node: add a new row with results
+ addRow(mCurrentValues);
+ } else {
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ mActiveTextDepthMask[i].set(currentNodeDepth, false);
+ }
+ }
+ currentNodeDepth--;
+ }
+
+ } else if ((eventType == XmlPullParser.TEXT) && (!parser.isWhitespace())) {
+ for (int i = 0; i < mNumberOfProjections; i++) {
+ if ((currentNodeDepth >= 0) &&
+ (mActiveTextDepthMask[i].get(currentNodeDepth))) {
+ mCurrentValues[i + 1] += parser.getText();
+ }
+ }
+ }
+
+ eventType = parser.next();
+ }
+ }
+ }
+}