//
//src\com\example\android\xmladapters\Adapters.java
/*
* 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 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;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Xml;
import android.view.View;
import android.widget.BaseAdapter;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
/**
* This class can be used to load {@link android.widget.Adapter adapters} defined in
* XML resources. XML-defined adapters can be used to easily create adapters in your
* own application or to pass adapters to other processes.
*
* Types of adapters
* Adapters defined using XML resources can only be one of the following supported
* types. Arbitrary adapters are not supported to guarantee the safety of the loaded
* code when adapters are loaded across packages.
*
* - Cursor adapter: a cursor adapter can be used
* to display the content of a cursor, most often coming from a content provider
*
* The complete XML format definition of each adapter type is available below.
*
*
* Cursor adapter
* A cursor adapter XML definition starts with the
* <cursor-adapter />
* tag and may contain one or more instances of the following tags:
*
* <select />
* <bind />
*
*
*
* <cursor-adapter />
* The <cursor-adapter />
element defines the beginning of the
* document and supports the following attributes:
*
* android:layout
: Reference to the XML layout to be inflated for
* each item of the adapter. This attribute is mandatory.
* android:selection
: Selection expression, used when the
* android:uri
attribute is defined or when the adapter is loaded with
* {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
* This attribute is optional.
* android:sortOrder
: Sort expression, used when the
* android:uri
attribute is defined or when the adapter is loaded with
* {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
* This attribute is optional.
* android:uri
: URI of the content provider to query to retrieve a cursor.
* Specifying this attribute is equivalent to calling
* {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
* If you call this method, the value of the XML attribute is ignored. This attribute is
* optional.
*
* In addition, you can specify one or more instances of
* <select />
and
* <bind />
tags as children
* of <cursor-adapter />
.
*
*
* <select />
* The <select />
tag is used to select columns from the cursor
* when doing the query. This can be very useful when using transformations in the
* <bind />
elements. It can also be very useful if you are providing
* your own binder or
* transformation classes.
* <select />
elements are ignored if you supply the cursor yourself.
* The <select />
supports the following attributes:
*
* android:column
: Name of the column to select in the cursor during the
* query operation
*
* Note: The column named _id
is always implicitly
* selected.
*
*
* <bind />
* The <bind />
tag is used to bind a column from the cursor to
* a {@link android.view.View}. A column bound using this tag is automatically selected
* during the query and a matching
* <select />
tag is therefore
* not required.
*
* Each binding is declared as a one to one matching but
* custom binder classes or special
* data transformations can
* allow you to bind several columns to a single view. In this case you must use the
* <select />
tag to make
* sure any required column is part of the query.
*
* The <bind />
tag supports the following attributes:
*
* android:from
: The name of the column to bind from.
* This attribute is mandatory. Note that @
which are not used to reference resources
* should be backslash protected as in \@
.
* android:to
: The id of the view to bind to. This attribute is mandatory.
* android:as
: The data type
* of the binding. This attribute is mandatory.
*
*
* In addition, a <bind />
can contain zero or more instances of
* data transformations children
* tags.
*
*
* Binding data types
* For a binding to occur the data type of the bound column/view pair must be specified.
* The following data types are currently supported:
*
* string
: The content of the column is interpreted as a string and must be
* bound to a {@link android.widget.TextView}
* image
: The content of the column is interpreted as a blob describing an
* image and must be bound to an {@link android.widget.ImageView}
* image-uri
: The content of the column is interpreted as a URI to an image
* and must be bound to an {@link android.widget.ImageView}
* drawable
: The content of the column is interpreted as a resource id to a
* drawable and must be bound to an {@link android.widget.ImageView}
* tag
: The content of the column is interpreted as a string and will be set as
* the tag (using {@link View#setTag(Object)} of the associated View. This can be used to
* associate meta-data to your view, that can be used for instance by a listener.
* - A fully qualified class name: The name of a class corresponding to an implementation of
* {@link Adapters.CursorBinder}. Cursor binders can be used to provide
* bindings not supported by default. Custom binders cannot be used with
* {@link android.content.Context#isRestricted() restricted contexts}, for instance in an
* application widget
*
*
*
* Binding transformations
* When defining a data binding you can specify an optional transformation by using one
* of the following tags as a child of a <bind />
elements:
*
* <map />
: Maps a constant string to a string or a resource. Use
* one instance of this tag per value you want to map
* <transform />
: Transforms a column's value using an expression
* or an instance of {@link Adapters.CursorTransformation}
*
* While several <map />
tags can be used at the same time, you cannot
* mix <map />
and <transform />
tags. If several
* <transform />
tags are specified, only the last one is retained.
*
*
* <map />
* A map element simply specifies a value to match from and a value to match to. When
* a column's value equals the value to match from, it is replaced with the value to match
* to. The following attributes are supported:
*
* android:fromValue
: The value to match from. This attribute is mandatory
* android:toValue
: The value to match to. This value can be either a string
* or a resource identifier. This value is interpreted as a resource identifier when the
* data binding is of type drawable
. This attribute is mandatory
*
*
*
* <transform />
* A simple transform that occurs either by calling a specified class or by performing
* simple text substitution. The following attributes are supported:
*
* android:withExpression
: The transformation expression. The expression is
* a string containing column names surrounded with curly braces { and }. During the
* transformation each column name is replaced by its value. All columns must have been
* selected in the query. An example of expression is "First name: {first_name},
* last name: {last_name}"
. This attribute is mandatory
* if android:withClass
is not specified and ignored if android:withClass
* is specified
* android:withClass
: A fully qualified class name corresponding to an
* implementation of {@link Adapters.CursorTransformation}. Custom
* transformations cannot be used with
* {@link android.content.Context#isRestricted() restricted contexts}, for instance in
* an app widget This attribute is mandatory if android:withExpression
is
* not specified
*
*
* Example
* The following example defines a cursor adapter that queries all the contacts with
* a phone number using the contacts content provider. Each contact is displayed with
* its display name, its favorite status and its photo. To display photos, a custom data
* binder is declared:
*
*
* <cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android"
* android:uri="content://com.android.contacts/contacts"
* android:selection="has_phone_number=1"
* android:layout="@layout/contact_item">
*
* <bind android:from="display_name" android:to="@id/name" android:as="string" />
* <bind android:from="starred" android:to="@id/star" android:as="drawable">
* <map android:fromValue="0" android:toValue="@android:drawable/star_big_off" />
* <map android:fromValue="1" android:toValue="@android:drawable/star_big_on" />
* </bind>
* <bind android:from="_id" android:to="@id/name"
* android:as="com.google.android.test.adapters.ContactPhotoBinder" />
*
* </cursor-adapter>
*
*
* Related APIs
*
* - {@link Adapters#loadAdapter(android.content.Context, int, Object[])}
* - {@link Adapters#loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])}
* - {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}
* - {@link Adapters.CursorBinder}
* - {@link Adapters.CursorTransformation}
* - {@link android.widget.CursorAdapter}
*
*
* @see android.widget.Adapter
* @see android.content.ContentProvider
*
* attr ref android.R.styleable#CursorAdapter_layout
* attr ref android.R.styleable#CursorAdapter_selection
* attr ref android.R.styleable#CursorAdapter_sortOrder
* attr ref android.R.styleable#CursorAdapter_uri
* attr ref android.R.styleable#CursorAdapter_BindItem_as
* attr ref android.R.styleable#CursorAdapter_BindItem_from
* attr ref android.R.styleable#CursorAdapter_BindItem_to
* attr ref android.R.styleable#CursorAdapter_MapItem_fromValue
* attr ref android.R.styleable#CursorAdapter_MapItem_toValue
* attr ref android.R.styleable#CursorAdapter_SelectItem_column
* 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";
/**
* Interface used to bind a {@link android.database.Cursor} column to a View. This
* interface can be used to provide bindings for data types not supported by the
* standard implementation of {@link Adapters}.
*
* A binder is provided with a cursor transformation which may or may not be used
* to transform the value retrieved from the cursor. The transformation is guaranteed
* to never be null so it's always safe to apply the transformation.
*
* The binder is associated with a Context but can be re-used with multiple cursors.
* As such, the implementation should make no assumption about the Cursor in use.
*
* @see android.view.View
* @see android.database.Cursor
* @see Adapters.CursorTransformation
*/
public static abstract class CursorBinder {
/**
* The context associated with this binder.
*/
protected final Context mContext;
/**
* The transformation associated with this binder. This transformation is never
* null and may or may not be applied to the Cursor data during the
* {@link #bind(android.view.View, android.database.Cursor, int)} operation.
*
* @see #bind(android.view.View, android.database.Cursor, int)
*/
protected final CursorTransformation mTransformation;
/**
* Creates a new Cursor binder.
*
* @param context The context associated with this binder.
* @param transformation The transformation associated with this binder. This
* transformation may or may not be applied by the binder and is guaranteed
* to not be null.
*/
public CursorBinder(Context context, CursorTransformation transformation) {
mContext = context;
mTransformation = transformation;
}
/**
* Binds the specified Cursor column to the supplied View. The binding operation
* can query other Cursor columns as needed. During the binding operation, values
* retrieved from the Cursor may or may not be transformed using this binder's
* cursor transformation.
*
* @param view The view to bind data to.
* @param cursor The cursor to bind data from.
* @param columnIndex The column index in the cursor where the data to bind resides.
*
* @see #mTransformation
*
* @return True if the column was successfully bound to the View, false otherwise.
*/
public abstract boolean bind(View view, Cursor cursor, int columnIndex);
}
/**
* Interface used to transform data coming out of a {@link android.database.Cursor}
* before it is bound to a {@link android.view.View}.
*
* Transformations are used to transform text-based data (in the form of a String),
* or to transform data into a resource identifier. A default implementation is provided
* to generate resource identifiers.
*
* @see android.database.Cursor
* @see Adapters.CursorBinder
*/
public static abstract class CursorTransformation {
/**
* The context associated with this transformation.
*/
protected final Context mContext;
/**
* Creates a new Cursor transformation.
*
* @param context The context associated with this transformation.
*/
public CursorTransformation(Context context) {
mContext = context;
}
/**
* Transforms the specified Cursor column into a String. The transformation
* can simply return the content of the column as a String (this is known
* as the identity transformation) or manipulate the content. For instance,
* a transformation can perform text substitutions or concatenate other
* columns with the specified column.
*
* @param cursor The cursor that contains the data to transform.
* @param columnIndex The index of the column to transform.
*
* @return A String containing the transformed value of the column.
*/
public abstract String transform(Cursor cursor, int columnIndex);
/**
* Transforms the specified Cursor column into a resource identifier.
* The default implementation simply interprets the content of the column
* as an integer.
*
* @param cursor The cursor that contains the data to transform.
* @param columnIndex The index of the column to transform.
*
* @return A resource identifier.
*/
public int transformToResource(Cursor cursor, int columnIndex) {
return cursor.getInt(columnIndex);
}
}
/**
* Loads the {@link android.widget.CursorAdapter} defined in the specified
* XML resource. The content of the adapter is loaded from the content provider
* identified by the supplied URI.
*
* Note: If the supplied {@link android.content.Context} is
* an {@link android.app.Activity}, the cursor returned by the content provider
* will be automatically managed. Otherwise, you are responsible for managing the
* cursor yourself.
*
* The format of the XML definition of the cursor adapter is documented at
* the top of this page.
*
* @param context The context to load the XML resource from.
* @param id The identifier of the XML resource declaring the adapter.
* @param uri The URI of the content provider.
* @param parameters Optional parameters to pass to the CursorAdapter, used
* to substitute values in the selection expression.
*
* @return A {@link android.widget.CursorAdapter}
*
* @throws IllegalArgumentException If the XML resource does not contain
* a valid <cursor-adapter /> definition.
*
* @see android.content.ContentProvider
* @see android.widget.CursorAdapter
* @see #loadAdapter(android.content.Context, int, Object[])
*/
public static CursorAdapter loadCursorAdapter(Context context, int id, String uri,
Object... parameters) {
XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR,
parameters);
if (uri != null) {
adapter.setUri(uri);
}
adapter.load();
return adapter;
}
/**
* Loads the {@link android.widget.CursorAdapter} defined in the specified
* XML resource. The content of the adapter is loaded from the specified cursor.
* You are responsible for managing the supplied cursor.
*
* The format of the XML definition of the cursor adapter is documented at
* the top of this page.
*
* @param context The context to load the XML resource from.
* @param id The identifier of the XML resource declaring the adapter.
* @param cursor The cursor containing the data for the adapter.
* @param parameters Optional parameters to pass to the CursorAdapter, used
* to substitute values in the selection expression.
*
* @return A {@link android.widget.CursorAdapter}
*
* @throws IllegalArgumentException If the XML resource does not contain
* a valid <cursor-adapter /> definition.
*
* @see android.content.ContentProvider
* @see android.widget.CursorAdapter
* @see android.database.Cursor
* @see #loadAdapter(android.content.Context, int, Object[])
*/
public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor,
Object... parameters) {
XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR,
parameters);
if (cursor != null) {
adapter.changeCursor(cursor);
}
return adapter;
}
/**
* Loads the adapter defined in the specified XML resource. The XML definition of
* the adapter must follow the format definition of one of the supported adapter
* types described at the top of this page.
*
* Note: If the loaded adapter is a {@link android.widget.CursorAdapter}
* and the supplied {@link android.content.Context} is an {@link android.app.Activity},
* the cursor returned by the content provider will be automatically managed. Otherwise,
* you are responsible for managing the cursor yourself.
*
* @param context The context to load the XML resource from.
* @param id The identifier of the XML resource declaring the adapter.
* @param parameters Optional parameters to pass to the adapter.
*
* @return An adapter instance.
*
* @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])
* @see #loadCursorAdapter(android.content.Context, int, String, Object[])
*/
public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) {
final BaseAdapter adapter = loadAdapter(context, id, null, parameters);
if (adapter instanceof ManagedAdapter) {
((ManagedAdapter) adapter).load();
}
return adapter;
}
/**
* Loads an adapter from the specified XML resource. The optional assertName can
* be used to exit early if the adapter defined in the XML resource is not of the
* expected type.
*
* @param context The context to associate with the adapter.
* @param id The resource id of the XML document defining the adapter.
* @param assertName The mandatory name of the adapter in the XML document.
* Ignored if null.
* @param parameters Optional parameters passed to the adapter.
*
* @return An instance of {@link android.widget.BaseAdapter}.
*/
private static BaseAdapter loadAdapter(Context context, int id, String assertName,
Object... parameters) {
XmlResourceParser parser = null;
try {
parser = context.getResources().getXml(id);
return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser),
id, parameters, assertName);
} catch (XmlPullParserException ex) {
Resources.NotFoundException rnf = new Resources.NotFoundException(
"Can't load adapter resource ID " +
context.getResources().getResourceEntryName(id));
rnf.initCause(ex);
throw rnf;
} catch (IOException ex) {
Resources.NotFoundException rnf = new Resources.NotFoundException(
"Can't load adapter resource ID " +
context.getResources().getResourceEntryName(id));
rnf.initCause(ex);
throw rnf;
} finally {
if (parser != null) parser.close();
}
}
/**
* Generates an adapter using the specified XML parser. This method is responsible
* for choosing the type of the adapter to create based on the content of the
* XML parser.
*
* This method will generate an {@link IllegalArgumentException} if
* assertName
is not null and does not match the root tag of the XML
* document.
*/
private static BaseAdapter createAdapterFromXml(Context c,
XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters,
String assertName) throws XmlPullParserException, IOException {
BaseAdapter adapter = null;
// Make sure we are on a start tag.
int type;
int depth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (assertName != null && !assertName.equals(name)) {
throw new IllegalArgumentException("The adapter defined in " +
c.getResources().getResourceEntryName(id) + " must be a <" +
assertName + " />");
}
if (ADAPTER_CURSOR.equals(name)) {
adapter = createCursorAdapter(c, parser, attrs, id, parameters);
} else {
throw new IllegalArgumentException("Unknown adapter name " + parser.getName() +
" in " + c.getResources().getResourceEntryName(id));
}
}
return adapter;
}
/**
* Creates an XmlCursorAdapter using an XmlCursorAdapterParser.
*/
private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser,
AttributeSet attrs, int id, Object[] parameters)
throws IOException, XmlPullParserException {
return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters);
}
/**
* Parser that can generate XmlCursorAdapter instances. This parser is responsible for
* handling all the attributes and child nodes for a <cursor-adapter />.
*/
private static class XmlCursorAdapterParser {
private static final String ADAPTER_CURSOR_BIND = "bind";
private static final String ADAPTER_CURSOR_SELECT = "select";
private static final String ADAPTER_CURSOR_AS_STRING = "string";
private static final String ADAPTER_CURSOR_AS_IMAGE = "image";
private static final String ADAPTER_CURSOR_AS_TAG = "tag";
private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri";
private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable";
private static final String ADAPTER_CURSOR_MAP = "map";
private static final String ADAPTER_CURSOR_TRANSFORM = "transform";
private final Context mContext;
private final XmlPullParser mParser;
private final AttributeSet mAttrs;
private final int mId;
private final HashMap mBinders;
private final ArrayList mFrom;
private final ArrayList mTo;
private final CursorTransformation mIdentity;
private final Resources mResources;
public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) {
mContext = c;
mParser = parser;
mAttrs = attrs;
mId = id;
mResources = mContext.getResources();
mBinders = new HashMap();
mFrom = new ArrayList();
mTo = new ArrayList();
mIdentity = new IdentityTransformation(mContext);
}
public XmlCursorAdapter parse(Object[] parameters)
throws IOException, XmlPullParserException {
Resources resources = mResources;
TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter);
String uri = a.getString(R.styleable.CursorAdapter_uri);
String selection = a.getString(R.styleable.CursorAdapter_selection);
String sortOrder = a.getString(R.styleable.CursorAdapter_sortOrder);
int layout = a.getResourceId(R.styleable.CursorAdapter_layout, 0);
if (layout == 0) {
throw new IllegalArgumentException("The layout specified in " +
resources.getResourceEntryName(mId) + " does not exist");
}
a.recycle();
XmlPullParser parser = mParser;
int type;
int depth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (ADAPTER_CURSOR_BIND.equals(name)) {
parseBindTag();
} else if (ADAPTER_CURSOR_SELECT.equals(name)) {
parseSelectTag();
} else {
throw new RuntimeException("Unknown tag name " + parser.getName() + " in " +
resources.getResourceEntryName(mId));
}
}
String[] fromArray = mFrom.toArray(new String[mFrom.size()]);
int[] toArray = new int[mTo.size()];
for (int i = 0; i < toArray.length; i++) {
toArray[i] = mTo.get(i);
}
String[] selectionArgs = null;
if (parameters != null) {
selectionArgs = new String[parameters.length];
for (int i = 0; i < selectionArgs.length; i++) {
selectionArgs[i] = (String) parameters[i];
}
}
return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection,
selectionArgs, sortOrder, mBinders);
}
private void parseSelectTag() {
TypedArray a = mResources.obtainAttributes(mAttrs,
R.styleable.CursorAdapter_SelectItem);
String fromName = a.getString(R.styleable.CursorAdapter_SelectItem_column);
if (fromName == null) {
throw new IllegalArgumentException("A select item in " +
mResources.getResourceEntryName(mId) +
" does not have a 'column' attribute");
}
a.recycle();
mFrom.add(fromName);
mTo.add(View.NO_ID);
}
private void parseBindTag() throws IOException, XmlPullParserException {
Resources resources = mResources;
TypedArray a = resources.obtainAttributes(mAttrs,
R.styleable.CursorAdapter_BindItem);
String fromName = a.getString(R.styleable.CursorAdapter_BindItem_from);
if (fromName == null) {
throw new IllegalArgumentException("A bind item in " +
resources.getResourceEntryName(mId) + " does not have a 'from' attribute");
}
int toName = a.getResourceId(R.styleable.CursorAdapter_BindItem_to, 0);
if (toName == 0) {
throw new IllegalArgumentException("A bind item in " +
resources.getResourceEntryName(mId) + " does not have a 'to' attribute");
}
String asType = a.getString(R.styleable.CursorAdapter_BindItem_as);
if (asType == null) {
throw new IllegalArgumentException("A bind item in " +
resources.getResourceEntryName(mId) + " does not have an 'as' attribute");
}
mFrom.add(fromName);
mTo.add(toName);
mBinders.put(fromName, findBinder(asType));
a.recycle();
}
private CursorBinder findBinder(String type) throws IOException, XmlPullParserException {
final XmlPullParser parser = mParser;
final Context context = mContext;
CursorTransformation transformation = mIdentity;
int tagType;
int depth = parser.getDepth();
final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type);
while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& tagType != XmlPullParser.END_DOCUMENT) {
if (tagType != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (ADAPTER_CURSOR_TRANSFORM.equals(name)) {
transformation = findTransformation();
} else if (ADAPTER_CURSOR_MAP.equals(name)) {
if (!(transformation instanceof MapTransformation)) {
transformation = new MapTransformation(context);
}
findMap(((MapTransformation) transformation), isDrawable);
} else {
throw new RuntimeException("Unknown tag name " + parser.getName() + " in " +
context.getResources().getResourceEntryName(mId));
}
}
if (ADAPTER_CURSOR_AS_STRING.equals(type)) {
return new StringBinder(context, transformation);
} else if (ADAPTER_CURSOR_AS_TAG.equals(type)) {
return new TagBinder(context, transformation);
} else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) {
return new ImageBinder(context, transformation);
} else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) {
return new ImageUriBinder(context, transformation);
} else if (isDrawable) {
return new DrawableBinder(context, transformation);
} else {
return createBinder(type, transformation);
}
}
private CursorBinder createBinder(String type, CursorTransformation transformation) {
if (mContext.isRestricted()) return null;
try {
final Class> klass = Class.forName(type, true, mContext.getClassLoader());
if (CursorBinder.class.isAssignableFrom(klass)) {
final Constructor> c = klass.getDeclaredConstructor(
Context.class, CursorTransformation.class);
return (CursorBinder) c.newInstance(mContext, transformation);
}
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Cannot instanciate binder type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Cannot instanciate binder type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Cannot instanciate binder type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
} catch (InstantiationException e) {
throw new IllegalArgumentException("Cannot instanciate binder type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Cannot instanciate binder type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
}
return null;
}
private void findMap(MapTransformation transformation, boolean drawable) {
Resources resources = mResources;
TypedArray a = resources.obtainAttributes(mAttrs,
R.styleable.CursorAdapter_MapItem);
String from = a.getString(R.styleable.CursorAdapter_MapItem_fromValue);
if (from == null) {
throw new IllegalArgumentException("A map item in " +
resources.getResourceEntryName(mId) +
" does not have a 'fromValue' attribute");
}
if (!drawable) {
String to = a.getString(R.styleable.CursorAdapter_MapItem_toValue);
if (to == null) {
throw new IllegalArgumentException("A map item in " +
resources.getResourceEntryName(mId) +
" does not have a 'toValue' attribute");
}
transformation.addStringMapping(from, to);
} else {
int to = a.getResourceId(R.styleable.CursorAdapter_MapItem_toValue, 0);
if (to == 0) {
throw new IllegalArgumentException("A map item in " +
resources.getResourceEntryName(mId) +
" does not have a 'toValue' attribute");
}
transformation.addResourceMapping(from, to);
}
a.recycle();
}
private CursorTransformation findTransformation() {
Resources resources = mResources;
CursorTransformation transformation = null;
TypedArray a = resources.obtainAttributes(mAttrs,
R.styleable.CursorAdapter_TransformItem);
String className = a.getString(R.styleable.CursorAdapter_TransformItem_withClass);
if (className == null) {
String expression = a.getString(
R.styleable.CursorAdapter_TransformItem_withExpression);
transformation = createExpressionTransformation(expression);
} else if (!mContext.isRestricted()) {
try {
final Class> klas = Class.forName(className, true, mContext.getClassLoader());
if (CursorTransformation.class.isAssignableFrom(klas)) {
final Constructor> c = klas.getDeclaredConstructor(Context.class);
transformation = (CursorTransformation) c.newInstance(mContext);
}
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Cannot instanciate transform type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Cannot instanciate transform type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Cannot instanciate transform type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
} catch (InstantiationException e) {
throw new IllegalArgumentException("Cannot instanciate transform type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Cannot instanciate transform type in " +
mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
}
}
a.recycle();
if (transformation == null) {
throw new IllegalArgumentException("A transform item in " +
resources.getResourceEntryName(mId) + " must have a 'withClass' or " +
"'withExpression' attribute");
}
return transformation;
}
private CursorTransformation createExpressionTransformation(String expression) {
return new ExpressionTransformation(mContext, expression);
}
}
/**
* Interface used by adapters that require to be loaded after creation.
*/
private static interface ManagedAdapter {
/**
* Loads the content of the adapter, asynchronously.
*/
void load();
}
/**
* Implementation of a Cursor adapter defined in XML. This class is a thin wrapper
* of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders.
*/
private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter {
private String mUri;
private final String mSelection;
private final String[] mSelectionArgs;
private final String mSortOrder;
private final String[] mColumns;
private final CursorBinder[] mBinders;
private AsyncTask mLoadTask;
XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to,
String selection, String[] selectionArgs, String sortOrder,
HashMap binders) {
super(context, layout, null, from, to);
mContext = context;
mUri = uri;
mSelection = selection;
mSelectionArgs = selectionArgs;
mSortOrder = sortOrder;
mColumns = new String[from.length + 1];
// This is mandatory in CursorAdapter
mColumns[0] = "_id";
System.arraycopy(from, 0, mColumns, 1, from.length);
CursorBinder basic = new StringBinder(context, new IdentityTransformation(context));
final int count = from.length;
mBinders = new CursorBinder[count];
for (int i = 0; i < count; i++) {
CursorBinder binder = binders.get(from[i]);
if (binder == null) binder = basic;
mBinders[i] = binder;
}
}
@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]);
}
}
}
public void load() {
if (mUri != null) {
mLoadTask = new QueryTask().execute();
}
}
void setUri(String uri) {
mUri = uri;
}
@Override
public void changeCursor(Cursor c) {
if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) {
mLoadTask.cancel(true);
mLoadTask = null;
}
super.changeCursor(c);
}
class QueryTask extends AsyncTask {
@Override
protected Cursor doInBackground(Void... params) {
if (mContext instanceof Activity) {
return ((Activity) mContext).managedQuery(
Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder);
} else {
return mContext.getContentResolver().query(
Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder);
}
}
@Override
protected void onPostExecute(Cursor cursor) {
if (!isCancelled()) {
XmlCursorAdapter.super.changeCursor(cursor);
}
}
}
}
/**
* Identity transformation, returns the content of the specified column as a String,
* without performing any manipulation. This is used when no transformation is specified.
*/
private static class IdentityTransformation extends CursorTransformation {
public IdentityTransformation(Context context) {
super(context);
}
@Override
public String transform(Cursor cursor, int columnIndex) {
return cursor.getString(columnIndex);
}
}
/**
* An expression transformation is a simple template based replacement utility.
* In an expression, each segment of the form {([^}]+)}
is replaced
* with the value of the column of name $1.
*/
private static class ExpressionTransformation extends CursorTransformation {
private final ExpressionNode mFirstNode = new ConstantExpressionNode("");
private final StringBuilder mBuilder = new StringBuilder();
public ExpressionTransformation(Context context, String expression) {
super(context);
parse(expression);
}
private void parse(String expression) {
ExpressionNode node = mFirstNode;
int segmentStart;
int count = expression.length();
for (int i = 0; i < count; i++) {
char c = expression.charAt(i);
// Start a column name segment
segmentStart = i;
if (c == '{') {
while (i < count && (c = expression.charAt(i)) != '}') {
i++;
}
// We've reached the end, but the expression didn't close
if (c != '}') {
throw new IllegalStateException("The transform expression contains a " +
"non-closed column name: " +
expression.substring(segmentStart + 1, i));
}
node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i));
} else {
while (i < count && (c = expression.charAt(i)) != '{') {
i++;
}
node.next = new ConstantExpressionNode(expression.substring(segmentStart, i));
// Rewind if we've reached a column expression
if (c == '{') i--;
}
node = node.next;
}
}
@Override
public String transform(Cursor cursor, int columnIndex) {
final StringBuilder builder = mBuilder;
builder.delete(0, builder.length());
ExpressionNode node = mFirstNode;
// Skip the first node
while ((node = node.next) != null) {
builder.append(node.asString(cursor));
}
return builder.toString();
}
static abstract class ExpressionNode {
public ExpressionNode next;
public abstract String asString(Cursor cursor);
}
static class ConstantExpressionNode extends ExpressionNode {
private final String mConstant;
ConstantExpressionNode(String constant) {
mConstant = constant;
}
@Override
public String asString(Cursor cursor) {
return mConstant;
}
}
static class ColumnExpressionNode extends ExpressionNode {
private final String mColumnName;
private Cursor mSignature;
private int mColumnIndex = -1;
ColumnExpressionNode(String columnName) {
mColumnName = columnName;
}
@Override
public String asString(Cursor cursor) {
if (cursor != mSignature || mColumnIndex == -1) {
mColumnIndex = cursor.getColumnIndex(mColumnName);
mSignature = cursor;
}
return cursor.getString(mColumnIndex);
}
}
}
/**
* A map transformation offers a simple mapping between specified String values
* to Strings or integers.
*/
private static class MapTransformation extends CursorTransformation {
private final HashMap mStringMappings;
private final HashMap mResourceMappings;
public MapTransformation(Context context) {
super(context);
mStringMappings = new HashMap();
mResourceMappings = new HashMap();
}
void addStringMapping(String from, String to) {
mStringMappings.put(from, to);
}
void addResourceMapping(String from, int to) {
mResourceMappings.put(from, to);
}
@Override
public String transform(Cursor cursor, int columnIndex) {
final String value = cursor.getString(columnIndex);
final String transformed = mStringMappings.get(value);
return transformed == null ? value : transformed;
}
@Override
public int transformToResource(Cursor cursor, int columnIndex) {
final String value = cursor.getString(columnIndex);
final Integer transformed = mResourceMappings.get(value);
try {
return transformed == null ? Integer.parseInt(value) : transformed;
} catch (NumberFormatException e) {
return 0;
}
}
}
/**
* Binds a String to a TextView.
*/
private static class StringBinder extends CursorBinder {
public StringBinder(Context context, CursorTransformation transformation) {
super(context, transformation);
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
if (view instanceof TextView) {
final String text = mTransformation.transform(cursor, columnIndex);
((TextView) view).setText(text);
return true;
}
return false;
}
}
/**
* Binds an image blob to an ImageView.
*/
private static class ImageBinder extends CursorBinder {
public ImageBinder(Context context, CursorTransformation transformation) {
super(context, transformation);
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
if (view instanceof ImageView) {
final byte[] data = cursor.getBlob(columnIndex);
((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0,
data.length));
return true;
}
return false;
}
}
private static class TagBinder extends CursorBinder {
public TagBinder(Context context, CursorTransformation transformation) {
super(context, transformation);
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
final String text = mTransformation.transform(cursor, columnIndex);
view.setTag(text);
return true;
}
}
/**
* Binds an image URI to an ImageView.
*/
private static class ImageUriBinder extends CursorBinder {
public ImageUriBinder(Context context, CursorTransformation transformation) {
super(context, transformation);
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
if (view instanceof ImageView) {
((ImageView) view).setImageURI(Uri.parse(
mTransformation.transform(cursor, columnIndex)));
return true;
}
return false;
}
}
/**
* Binds a drawable resource identifier to an ImageView.
*/
private static class DrawableBinder extends CursorBinder {
public DrawableBinder(Context context, CursorTransformation transformation) {
super(context, transformation);
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
if (view instanceof ImageView) {
final int resource = mTransformation.transformToResource(cursor, columnIndex);
if (resource == 0) return false;
((ImageView) view).setImageResource(resource);
return true;
}
return false;
}
}
}
//src\com\example\android\xmladapters\ContactPhotoBinder.java
/*
* 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 com.example.android.xmladapters;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract;
import android.view.View;
import android.widget.TextView;
import java.io.InputStream;
import java.util.HashMap;
/**
* This custom cursor binder is used by the adapter defined in res/xml to
* bind contacts photos to their respective list item. This binder simply
* queries a contact's photo based on the contact's id and sets the
* photo as a compound drawable on the TextView used to display the contact's
* name.
*/
public class ContactPhotoBinder extends Adapters.CursorBinder {
private static final int PHOTO_SIZE_DIP = 54;
private final Drawable mDefault;
private final HashMap mCache;
private final Resources mResources;
private final int mPhotoSize;
public ContactPhotoBinder(Context context, Adapters.CursorTransformation transformation) {
super(context, transformation);
mResources = mContext.getResources();
// Default picture used when a contact does not provide one
mDefault = mResources.getDrawable(R.drawable.ic_contact_picture);
// Cache used to avoid re-querying contacts photos every time
mCache = new HashMap();
// Compute the size of the photo based on the display's density
mPhotoSize = (int) (PHOTO_SIZE_DIP * mResources.getDisplayMetrics().density + 0.5f);
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
final long id = cursor.getLong(columnIndex);
// First check whether we have already cached the contact's photo
Drawable d = mCache.get(id);
if (d == null) {
// If the photo wasn't in the cache, ask the contacts provider for
// an input stream we can use to load the photo
Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
InputStream stream = ContactsContract.Contacts.openContactPhotoInputStream(
mContext.getContentResolver(), uri);
// Creates the drawable for the contact's photo or use our fallback drawable
if (stream != null) {
// decoding the bitmap could be done in a worker thread too.
Bitmap bitmap = BitmapFactory.decodeStream(stream);
d = new BitmapDrawable(mResources, bitmap);
} else {
d = mDefault;
}
d.setBounds(0, 0, mPhotoSize, mPhotoSize);
((TextView) view).setCompoundDrawables(d, null, null, null);
// Remember the photo associated with this contact
mCache.put(id, d);
} else {
((TextView) view).setCompoundDrawables(d, null, null, null);
}
return true;
}
}
//src\com\example\android\xmladapters\ContactsListActivity.java
/*
* 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 com.example.android.xmladapters;
import android.app.ListActivity;
import android.os.Bundle;
/**
* This activity demonstrates how to create a complex UI using a ListView
* and an adapter defined in XML.
*
* The following activity shows a list of contacts, their starred status
* and their photos, using the adapter defined in res/xml.
*/
public class ContactsListActivity extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contacts_list);
setListAdapter(Adapters.loadAdapter(this, R.xml.contacts));
}
}
//src\com\example\android\xmladapters\ImageDownloader.java
/*
* 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 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.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.http.AndroidHttpClient;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import android.widget.ImageView;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* This helper class download images from the Internet and binds those with the provided ImageView.
*
* It requires the INTERNET permission, which should be added to your application's manifest
* file.
*
* A local cache of downloaded images is maintained internally to improve performance.
*/
public class ImageDownloader {
private static final String LOG_TAG = "ImageDownloader";
private static final int HARD_CACHE_CAPACITY = 40;
private static final int DELAY_BEFORE_PURGE = 30 * 1000; // in milliseconds
// Hard cache, with a fixed maximum capacity and a life duration
private final HashMap sHardBitmapCache =
new LinkedHashMap(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
@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
return false;
}
};
// Soft cache for bitmap kicked out of hard cache
private final static ConcurrentHashMap> sSoftBitmapCache =
new ConcurrentHashMap>(HARD_CACHE_CAPACITY / 2);
private final Handler purgeHandler = new Handler();
private final Runnable purger = new Runnable() {
public void run() {
clearCache();
}
};
/**
* Download the specified image from the Internet and binds it to the provided ImageView. The
* binding is immediate if the image is found in the cache and will be done asynchronously
* otherwise. A null bitmap will be associated to the ImageView if an error occurs.
*
* @param url The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void download(String url, ImageView imageView) {
download(url, imageView, null);
}
/**
* Same as {@link #download(String, ImageView)}, with the possibility to provide an additional
* cookie that will be used when the image will be retrieved.
*
* @param url The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
* @param cookie A cookie String that will be used by the http connection.
*/
public void download(String url, ImageView imageView, String cookie) {
resetPurgeTimer();
Bitmap bitmap = getBitmapFromCache(url);
if (bitmap == null) {
forceDownload(url, imageView, cookie);
} else {
cancelPotentialDownload(url, imageView);
imageView.setImageBitmap(bitmap);
}
}
/*
* Same as download but the image is always downloaded and the cache is not used.
* Kept private at the moment as its interest is not clear.
private void forceDownload(String url, ImageView view) {
forceDownload(url, view, null);
}
*/
/**
* Same as download but the image is always downloaded and the cache is not used.
* Kept private at the moment as its interest is not clear.
*/
private void forceDownload(String url, ImageView imageView, String cookie) {
// State sanity: url is guaranteed to never be null in DownloadedDrawable and cache keys.
if (url == null) {
imageView.setImageDrawable(null);
return;
}
if (cancelPotentialDownload(url, imageView)) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
task.execute(url, cookie);
}
}
/**
* Clears the image cache used internally to improve performance. Note that for memory
* efficiency reasons, the cache will automatically be cleared after a certain inactivity delay.
*/
public void clearCache() {
sHardBitmapCache.clear();
sSoftBitmapCache.clear();
}
private void resetPurgeTimer() {
purgeHandler.removeCallbacks(purger);
purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
}
/**
* Returns true if the current download has been canceled or if there was no download in
* progress on this image view.
* Returns false if the download in progress deals with the same url. The download is not
* stopped in that case.
*/
private static boolean cancelPotentialDownload(String url, ImageView imageView) {
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
if (bitmapDownloaderTask != null) {
String bitmapUrl = bitmapDownloaderTask.url;
if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
bitmapDownloaderTask.cancel(true);
} else {
// The same URL is already being downloaded.
return false;
}
}
return true;
}
/**
* @param imageView Any imageView
* @return Retrieve the currently active download task (if any) associated with this imageView.
* null if there is no such task.
*/
private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
return null;
}
/**
* @param url The URL of the image that will be retrieved from the cache.
* @return The cached bitmap or null if it was not found.
*/
private Bitmap getBitmapFromCache(String url) {
// First try the hard reference cache
synchronized (sHardBitmapCache) {
final Bitmap bitmap = sHardBitmapCache.get(url);
if (bitmap != null) {
// Bitmap found in hard cache
// Move element to first position, so that it is removed last
sHardBitmapCache.remove(url);
sHardBitmapCache.put(url, bitmap);
return bitmap;
}
}
// Then try the soft reference cache
SoftReference bitmapReference = sSoftBitmapCache.get(url);
if (bitmapReference != null) {
final Bitmap bitmap = bitmapReference.get();
if (bitmap != null) {
// Bitmap found in soft cache
return bitmap;
} else {
// Soft reference has been Garbage Collected
sSoftBitmapCache.remove(url);
}
}
return null;
}
/**
* The actual AsyncTask that will asynchronously download the image.
*/
class BitmapDownloaderTask extends AsyncTask {
private static final int IO_BUFFER_SIZE = 4 * 1024;
private String url;
private final WeakReference imageViewReference;
public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference(imageView);
}
/**
* Actual download method.
*/
@Override
protected Bitmap doInBackground(String... params) {
final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
url = params[0];
final HttpGet getRequest = new HttpGet(url);
String cookie = params[1];
if (cookie != null) {
getRequest.setHeader("cookie", cookie);
}
try {
HttpResponse response = client.execute(getRequest);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w("ImageDownloader", "Error " + statusCode +
" while retrieving bitmap from " + url);
return null;
}
final HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = entity.getContent();
final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
outputStream = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE);
copy(inputStream, outputStream);
outputStream.flush();
final byte[] data = dataStream.toByteArray();
final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
// FIXME : Should use BitmapFactory.decodeStream(inputStream) instead.
//final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
entity.consumeContent();
}
}
} catch (IOException e) {
getRequest.abort();
Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e);
} catch (IllegalStateException e) {
getRequest.abort();
Log.w(LOG_TAG, "Incorrect URL: " + url);
} catch (Exception e) {
getRequest.abort();
Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e);
} finally {
if (client != null) {
client.close();
}
}
return null;
}
/**
* Once the image is downloaded, associates it to the imageView
*/
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
// Add bitmap to cache
if (bitmap != null) {
synchronized (sHardBitmapCache) {
sHardBitmapCache.put(url, bitmap);
}
}
if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
// Change bitmap only if this process is still associated with it
if (this == bitmapDownloaderTask) {
imageView.setImageBitmap(bitmap);
}
}
}
public void copy(InputStream in, OutputStream out) throws IOException {
byte[] b = new byte[IO_BUFFER_SIZE];
int read;
while ((read = in.read(b)) != -1) {
out.write(b, 0, read);
}
}
}
/**
* A fake Drawable that will be attached to the imageView while the download is in progress.
*
* Contains a reference to the actual download task, so that a download task can be stopped
* if a new binding is required, and makes sure that only the last started download process can
* bind its result, independently of the download finish order.
*/
static class DownloadedDrawable extends ColorDrawable {
private final WeakReference bitmapDownloaderTaskReference;
public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
bitmapDownloaderTaskReference =
new WeakReference(bitmapDownloaderTask);
}
public BitmapDownloaderTask getBitmapDownloaderTask() {
return bitmapDownloaderTaskReference.get();
}
}
}
//src\com\example\android\xmladapters\PhotosListActivity.java
/*
* 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 com.example.android.xmladapters;
import android.app.ListActivity;
import android.net.Uri;
import android.os.Bundle;
/**
* This activity uses a custom cursor adapter which fetches a XML photo feed and parses the XML to
* extract the images' URL and their title.
*/
public class PhotosListActivity extends ListActivity {
private static final String PICASA_FEED_URL =
"http://picasaweb.google.com/data/feed/api/featured?max-results=50&thumbsize=144c";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.photos_list);
setListAdapter(Adapters.loadCursorAdapter(this, R.xml.photos,
"content://xmldocument/?url=" + Uri.encode(PICASA_FEED_URL)));
}
}
//src\com\example\android\xmladapters\RssReaderActivity.java
/*
* 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 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;
/**
* This example demonstrate the creation of a simple RSS feed reader using the XML adapter syntax.
* The different elements of the feed are extracted using an {@link XmlDocumentProvider} and are
* binded to the different views. An {@link OnItemClickListener} is also added, which will open a
* browser on the associated news item page.
*/
public class RssReaderActivity extends ListActivity {
private static final String FEED_URI = "http://feeds.nytimes.com/nyt/rss/HomePage";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.rss_feeds_list);
setListAdapter(Adapters.loadCursorAdapter(this, R.xml.rss_feed,
"content://xmldocument/?url=" + Uri.encode(FEED_URI)));
getListView().setOnItemClickListener(new UrlIntentListener());
}
}
//src\com\example\android\xmladapters\UrlImageBinder.java
/*
* 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 com.example.android.xmladapters;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.widget.ImageView;
/**
* This CursorBinder binds the provided image URL to an ImageView by downloading the image from the
* Internet.
*/
public class UrlImageBinder extends Adapters.CursorBinder {
private final ImageDownloader imageDownloader;
public UrlImageBinder(Context context, Adapters.CursorTransformation transformation) {
super(context, transformation);
imageDownloader = new ImageDownloader();
}
@Override
public boolean bind(View view, Cursor cursor, int columnIndex) {
if (view instanceof ImageView) {
final String url = mTransformation.transform(cursor, columnIndex);
imageDownloader.download(url, (ImageView) view);
return true;
}
return false;
}
}
//src\com\example\android\xmladapters\UrlIntentListener.java
/*
* 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 com.example.android.xmladapters;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
/**
* A listener which expects a URL as a tag of the view it is associated with. It then opens the URL
* in the browser application.
*/
public class UrlIntentListener implements OnItemClickListener {
public void onItemClick(AdapterView> parent, View view, int position, long id) {
final String url = view.getTag().toString();
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Context context = parent.getContext();
context.startActivity(intent);
}
}
//
//res\layout\contact_item.xml
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight">
android:id="@+id/name"
android:layout_width="0px"
android:layout_weight="1.0"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center_vertical"
android:drawablePadding="6dip"
android:paddingLeft="6dip"
android:paddingRight="6dip" />
android:id="@+id/star"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
//res\layout\contacts_list.xml
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_contacts"
android:visibility="gone" />
//res\layout\photo_item.xml
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeight">
android:id="@+id/photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dip"
android:paddingTop="4dip"
android:paddingBottom="4dip" />
android:id="@+id/title"
android:layout_width="0px"
android:layout_weight="1.0"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:paddingRight="6dip" />
//res\layout\photos_list.xml
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_photos"
android:visibility="gone" />
//res\layout\rss_feed_item.xml
android:id="@+id/item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:minHeight="?android:attr/listPreferredItemHeight">
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_weight="1.0"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:gravity="left"
android:paddingLeft="6dip"
android:paddingRight="6dip" />
android:id="@+id/date"
android:layout_width="fill_parent"
android:layout_weight="1.0"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="left"
android:paddingLeft="6dip"
android:paddingRight="6dip" />
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="?android:attr/listPreferredItemHeight">
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:paddingLeft="6dip" />
android:id="@+id/description"
android:layout_width="fill_parent"
android:layout_weight="1.0"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="left"
android:paddingLeft="6dip"
android:paddingRight="6dip" />
//res\layout\rss_feeds_list.xml
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_rss_feed"
android:visibility="gone" />
//
//res\values\attrs.xml
//res\values\strings.xml
Xml Adapters
Xml Contacts Adapter
Xml Photos Adapter
Xml RSS Reader
No contacts available
Loading photos...
Loading RSS feed...
//
//res\xml\contacts.xml
xmlns:app="http://schemas.android.com/apk/res/com.example.android.xmladapters"
app:uri="content://com.android.contacts/contacts"
app:selection="has_phone_number=1"
app:layout="@layout/contact_item">
//res\xml\photos.xml
xmlns:app="http://schemas.android.com/apk/res/com.example.android.xmladapters"
app:selection="/feed/entry"
app:layout="@layout/photo_item">
app:as="com.example.android.xmladapters.UrlImageBinder" />
//res\xml\rss_feed.xml
xmlns:app="http://schemas.android.com/apk/res/com.example.android.xmladapters"
app:selection="/rss/channel/item"
app:layout="@layout/rss_feed_item">