1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.xmladapters; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.content.res.TypedArray; 23 import android.content.res.XmlResourceParser; 24 import android.database.Cursor; 25 import android.graphics.BitmapFactory; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.util.AttributeSet; 29 import android.util.Xml; 30 import android.view.View; 31 import android.widget.BaseAdapter; 32 import android.widget.CursorAdapter; 33 import android.widget.ImageView; 34 import android.widget.SimpleCursorAdapter; 35 import android.widget.TextView; 36 37 import org.xmlpull.v1.XmlPullParser; 38 import org.xmlpull.v1.XmlPullParserException; 39 40 import java.io.IOException; 41 import java.lang.reflect.Constructor; 42 import java.lang.reflect.InvocationTargetException; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 46 /** 47 * <p>This class can be used to load {@link android.widget.Adapter adapters} defined in 48 * XML resources. XML-defined adapters can be used to easily create adapters in your 49 * own application or to pass adapters to other processes.</p> 50 * 51 * <h2>Types of adapters</h2> 52 * <p>Adapters defined using XML resources can only be one of the following supported 53 * types. Arbitrary adapters are not supported to guarantee the safety of the loaded 54 * code when adapters are loaded across packages.</p> 55 * <ul> 56 * <li><a href="#xml-cursor-adapter">Cursor adapter</a>: a cursor adapter can be used 57 * to display the content of a cursor, most often coming from a content provider</li> 58 * </ul> 59 * <p>The complete XML format definition of each adapter type is available below.</p> 60 * 61 * <a name="xml-cursor-adapter"></a> 62 * <h2>Cursor adapter</h2> 63 * <p>A cursor adapter XML definition starts with the 64 * <a href="#xml-cursor-adapter-tag"><code><cursor-adapter /></code></a> 65 * tag and may contain one or more instances of the following tags:</p> 66 * <ul> 67 * <li><a href="#xml-cursor-adapter-select-tag"><code><select /></code></a></li> 68 * <li><a href="#xml-cursor-adapter-bind-tag"><code><bind /></code></a></li> 69 * </ul> 70 * 71 * <a name="xml-cursor-adapter-tag"></a> 72 * <h3><cursor-adapter /></h3> 73 * <p>The <code><cursor-adapter /></code> element defines the beginning of the 74 * document and supports the following attributes:</p> 75 * <ul> 76 * <li><code>android:layout</code>: Reference to the XML layout to be inflated for 77 * each item of the adapter. This attribute is mandatory.</li> 78 * <li><code>android:selection</code>: Selection expression, used when the 79 * <code>android:uri</code> attribute is defined or when the adapter is loaded with 80 * {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. 81 * This attribute is optional.</li> 82 * <li><code>android:sortOrder</code>: Sort expression, used when the 83 * <code>android:uri</code> attribute is defined or when the adapter is loaded with 84 * {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. 85 * This attribute is optional.</li> 86 * <li><code>android:uri</code>: URI of the content provider to query to retrieve a cursor. 87 * Specifying this attribute is equivalent to calling 88 * {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}. 89 * If you call this method, the value of the XML attribute is ignored. This attribute is 90 * optional.</li> 91 * </ul> 92 * <p>In addition, you can specify one or more instances of 93 * <a href="#xml-cursor-adapter-select-tag"><code><select /></code></a> and 94 * <a href="#xml-cursor-adapter-bind-tag"><code><bind /></code></a> tags as children 95 * of <code><cursor-adapter /></code>.</p> 96 * 97 * <a name="xml-cursor-adapter-select-tag"></a> 98 * <h3><select /></h3> 99 * <p>The <code><select /></code> tag is used to select columns from the cursor 100 * when doing the query. This can be very useful when using transformations in the 101 * <code><bind /></code> elements. It can also be very useful if you are providing 102 * your own <a href="#xml-cursor-adapter-bind-data-types">binder</a> or 103 * <a href="#xml-cursor-adapter-bind-data-types">transformation</a> classes. 104 * <code><select /></code> elements are ignored if you supply the cursor yourself.</p> 105 * <p>The <code><select /></code> supports the following attributes:</p> 106 * <ul> 107 * <li><code>android:column</code>: Name of the column to select in the cursor during the 108 * query operation</li> 109 * </ul> 110 * <p><strong>Note:</strong> The column named <code>_id</code> is always implicitly 111 * selected.</p> 112 * 113 * <a name="xml-cursor-adapter-bind-tag"></a> 114 * <h3><bind /></h3> 115 * <p>The <code><bind /></code> tag is used to bind a column from the cursor to 116 * a {@link android.view.View}. A column bound using this tag is automatically selected 117 * during the query and a matching 118 * <a href="#xml-cursor-adapter-select-tag"><code><select /></code> tag is therefore 119 * not required.</p> 120 * 121 * <p>Each binding is declared as a one to one matching but 122 * custom binder classes or special 123 * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> can 124 * allow you to bind several columns to a single view. In this case you must use the 125 * <a href="#xml-cursor-adapter-select-tag"><code><select /></code> tag to make 126 * sure any required column is part of the query.</p> 127 * 128 * <p>The <code><bind /></code> tag supports the following attributes:</p> 129 * <ul> 130 * <li><code>android:from</code>: The name of the column to bind from. 131 * This attribute is mandatory. Note that <code>@</code> which are not used to reference resources 132 * should be backslash protected as in <code>\@</code>.</li> 133 * <li><code>android:to</code>: The id of the view to bind to. This attribute is mandatory.</li> 134 * <li><code>android:as</code>: The <a href="#xml-cursor-adapter-bind-data-types">data type</a> 135 * of the binding. This attribute is mandatory.</li> 136 * </ul> 137 * 138 * <p>In addition, a <code><bind /></code> can contain zero or more instances of 139 * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> children 140 * tags.</p> 141 * 142 * <a name="xml-cursor-adapter-bind-data-types"></a> 143 * <h4>Binding data types</h4> 144 * <p>For a binding to occur the data type of the bound column/view pair must be specified. 145 * The following data types are currently supported:</p> 146 * <ul> 147 * <li><code>string</code>: The content of the column is interpreted as a string and must be 148 * bound to a {@link android.widget.TextView}</li> 149 * <li><code>image</code>: The content of the column is interpreted as a blob describing an 150 * image and must be bound to an {@link android.widget.ImageView}</li> 151 * <li><code>image-uri</code>: The content of the column is interpreted as a URI to an image 152 * and must be bound to an {@link android.widget.ImageView}</li> 153 * <li><code>drawable</code>: The content of the column is interpreted as a resource id to a 154 * drawable and must be bound to an {@link android.widget.ImageView}</li> 155 * <li><code>tag</code>: The content of the column is interpreted as a string and will be set as 156 * the tag (using {@link View#setTag(Object)} of the associated View. This can be used to 157 * associate meta-data to your view, that can be used for instance by a listener.</li> 158 * <li>A fully qualified class name: The name of a class corresponding to an implementation of 159 * {@link Adapters.CursorBinder}. Cursor binders can be used to provide 160 * bindings not supported by default. Custom binders cannot be used with 161 * {@link android.content.Context#isRestricted() restricted contexts}, for instance in an 162 * application widget</li> 163 * </ul> 164 * 165 * <a name="xml-cursor-adapter-bind-transformation"></a> 166 * <h4>Binding transformations</h4> 167 * <p>When defining a data binding you can specify an optional transformation by using one 168 * of the following tags as a child of a <code><bind /></code> elements:</p> 169 * <ul> 170 * <li><code><map /></code>: Maps a constant string to a string or a resource. Use 171 * one instance of this tag per value you want to map</li> 172 * <li><code><transform /></code>: Transforms a column's value using an expression 173 * or an instance of {@link Adapters.CursorTransformation}</li> 174 * </ul> 175 * <p>While several <code><map /></code> tags can be used at the same time, you cannot 176 * mix <code><map /></code> and <code><transform /></code> tags. If several 177 * <code><transform /></code> tags are specified, only the last one is retained.</p> 178 * 179 * <a name="xml-cursor-adapter-bind-transformation-map" /> 180 * <p><strong><map /></strong></p> 181 * <p>A map element simply specifies a value to match from and a value to match to. When 182 * a column's value equals the value to match from, it is replaced with the value to match 183 * to. The following attributes are supported:</p> 184 * <ul> 185 * <li><code>android:fromValue</code>: The value to match from. This attribute is mandatory</li> 186 * <li><code>android:toValue</code>: The value to match to. This value can be either a string 187 * or a resource identifier. This value is interpreted as a resource identifier when the 188 * data binding is of type <code>drawable</code>. This attribute is mandatory</li> 189 * </ul> 190 * 191 * <a name="xml-cursor-adapter-bind-transformation-transform"></a> 192 * <p><strong><transform /></strong></p> 193 * <p>A simple transform that occurs either by calling a specified class or by performing 194 * simple text substitution. The following attributes are supported:</p> 195 * <ul> 196 * <li><code>android:withExpression</code>: The transformation expression. The expression is 197 * a string containing column names surrounded with curly braces { and }. During the 198 * transformation each column name is replaced by its value. All columns must have been 199 * selected in the query. An example of expression is <code>"First name: {first_name}, 200 * last name: {last_name}"</code>. This attribute is mandatory 201 * if <code>android:withClass</code> is not specified and ignored if <code>android:withClass</code> 202 * is specified</li> 203 * <li><code>android:withClass</code>: A fully qualified class name corresponding to an 204 * implementation of {@link Adapters.CursorTransformation}. Custom 205 * transformations cannot be used with 206 * {@link android.content.Context#isRestricted() restricted contexts}, for instance in 207 * an app widget This attribute is mandatory if <code>android:withExpression</code> is 208 * not specified</li> 209 * </ul> 210 * 211 * <h3>Example</h3> 212 * <p>The following example defines a cursor adapter that queries all the contacts with 213 * a phone number using the contacts content provider. Each contact is displayed with 214 * its display name, its favorite status and its photo. To display photos, a custom data 215 * binder is declared:</p> 216 * 217 * <pre class="prettyprint"> 218 * <cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android" 219 * android:uri="content://com.android.contacts/contacts" 220 * android:selection="has_phone_number=1" 221 * android:layout="@layout/contact_item"> 222 * 223 * <bind android:from="display_name" android:to="@id/name" android:as="string" /> 224 * <bind android:from="starred" android:to="@id/star" android:as="drawable"> 225 * <map android:fromValue="0" android:toValue="@android:drawable/star_big_off" /> 226 * <map android:fromValue="1" android:toValue="@android:drawable/star_big_on" /> 227 * </bind> 228 * <bind android:from="_id" android:to="@id/name" 229 * android:as="com.google.android.test.adapters.ContactPhotoBinder" /> 230 * 231 * </cursor-adapter> 232 * </pre> 233 * 234 * <h3>Related APIs</h3> 235 * <ul> 236 * <li>{@link Adapters#loadAdapter(android.content.Context, int, Object[])}</li> 237 * <li>{@link Adapters#loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])}</li> 238 * <li>{@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}</li> 239 * <li>{@link Adapters.CursorBinder}</li> 240 * <li>{@link Adapters.CursorTransformation}</li> 241 * <li>{@link android.widget.CursorAdapter}</li> 242 * </ul> 243 * 244 * @see android.widget.Adapter 245 * @see android.content.ContentProvider 246 * 247 * attr ref android.R.styleable#CursorAdapter_layout 248 * attr ref android.R.styleable#CursorAdapter_selection 249 * attr ref android.R.styleable#CursorAdapter_sortOrder 250 * attr ref android.R.styleable#CursorAdapter_uri 251 * attr ref android.R.styleable#CursorAdapter_BindItem_as 252 * attr ref android.R.styleable#CursorAdapter_BindItem_from 253 * attr ref android.R.styleable#CursorAdapter_BindItem_to 254 * attr ref android.R.styleable#CursorAdapter_MapItem_fromValue 255 * attr ref android.R.styleable#CursorAdapter_MapItem_toValue 256 * attr ref android.R.styleable#CursorAdapter_SelectItem_column 257 * attr ref android.R.styleable#CursorAdapter_TransformItem_withClass 258 * attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression 259 */ 260 public class Adapters { 261 private static final String ADAPTER_CURSOR = "cursor-adapter"; 262 263 /** 264 * <p>Interface used to bind a {@link android.database.Cursor} column to a View. This 265 * interface can be used to provide bindings for data types not supported by the 266 * standard implementation of {@link Adapters}.</p> 267 * 268 * <p>A binder is provided with a cursor transformation which may or may not be used 269 * to transform the value retrieved from the cursor. The transformation is guaranteed 270 * to never be null so it's always safe to apply the transformation.</p> 271 * 272 * <p>The binder is associated with a Context but can be re-used with multiple cursors. 273 * As such, the implementation should make no assumption about the Cursor in use.</p> 274 * 275 * @see android.view.View 276 * @see android.database.Cursor 277 * @see Adapters.CursorTransformation 278 */ 279 public static abstract class CursorBinder { 280 /** 281 * <p>The context associated with this binder.</p> 282 */ 283 protected final Context mContext; 284 285 /** 286 * <p>The transformation associated with this binder. This transformation is never 287 * null and may or may not be applied to the Cursor data during the 288 * {@link #bind(android.view.View, android.database.Cursor, int)} operation.</p> 289 * 290 * @see #bind(android.view.View, android.database.Cursor, int) 291 */ 292 protected final CursorTransformation mTransformation; 293 294 /** 295 * <p>Creates a new Cursor binder.</p> 296 * 297 * @param context The context associated with this binder. 298 * @param transformation The transformation associated with this binder. This 299 * transformation may or may not be applied by the binder and is guaranteed 300 * to not be null. 301 */ 302 public CursorBinder(Context context, CursorTransformation transformation) { 303 mContext = context; 304 mTransformation = transformation; 305 } 306 307 /** 308 * <p>Binds the specified Cursor column to the supplied View. The binding operation 309 * can query other Cursor columns as needed. During the binding operation, values 310 * retrieved from the Cursor may or may not be transformed using this binder's 311 * cursor transformation.</p> 312 * 313 * @param view The view to bind data to. 314 * @param cursor The cursor to bind data from. 315 * @param columnIndex The column index in the cursor where the data to bind resides. 316 * 317 * @see #mTransformation 318 * 319 * @return True if the column was successfully bound to the View, false otherwise. 320 */ 321 public abstract boolean bind(View view, Cursor cursor, int columnIndex); 322 } 323 324 /** 325 * <p>Interface used to transform data coming out of a {@link android.database.Cursor} 326 * before it is bound to a {@link android.view.View}.</p> 327 * 328 * <p>Transformations are used to transform text-based data (in the form of a String), 329 * or to transform data into a resource identifier. A default implementation is provided 330 * to generate resource identifiers.</p> 331 * 332 * @see android.database.Cursor 333 * @see Adapters.CursorBinder 334 */ 335 public static abstract class CursorTransformation { 336 /** 337 * <p>The context associated with this transformation.</p> 338 */ 339 protected final Context mContext; 340 341 /** 342 * <p>Creates a new Cursor transformation.</p> 343 * 344 * @param context The context associated with this transformation. 345 */ 346 public CursorTransformation(Context context) { 347 mContext = context; 348 } 349 350 /** 351 * <p>Transforms the specified Cursor column into a String. The transformation 352 * can simply return the content of the column as a String (this is known 353 * as the identity transformation) or manipulate the content. For instance, 354 * a transformation can perform text substitutions or concatenate other 355 * columns with the specified column.</p> 356 * 357 * @param cursor The cursor that contains the data to transform. 358 * @param columnIndex The index of the column to transform. 359 * 360 * @return A String containing the transformed value of the column. 361 */ 362 public abstract String transform(Cursor cursor, int columnIndex); 363 364 /** 365 * <p>Transforms the specified Cursor column into a resource identifier. 366 * The default implementation simply interprets the content of the column 367 * as an integer.</p> 368 * 369 * @param cursor The cursor that contains the data to transform. 370 * @param columnIndex The index of the column to transform. 371 * 372 * @return A resource identifier. 373 */ 374 public int transformToResource(Cursor cursor, int columnIndex) { 375 return cursor.getInt(columnIndex); 376 } 377 } 378 379 /** 380 * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified 381 * XML resource. The content of the adapter is loaded from the content provider 382 * identified by the supplied URI.</p> 383 * 384 * <p><strong>Note:</strong> If the supplied {@link android.content.Context} is 385 * an {@link android.app.Activity}, the cursor returned by the content provider 386 * will be automatically managed. Otherwise, you are responsible for managing the 387 * cursor yourself.</p> 388 * 389 * <p>The format of the XML definition of the cursor adapter is documented at 390 * the top of this page.</p> 391 * 392 * @param context The context to load the XML resource from. 393 * @param id The identifier of the XML resource declaring the adapter. 394 * @param uri The URI of the content provider. 395 * @param parameters Optional parameters to pass to the CursorAdapter, used 396 * to substitute values in the selection expression. 397 * 398 * @return A {@link android.widget.CursorAdapter} 399 * 400 * @throws IllegalArgumentException If the XML resource does not contain 401 * a valid <cursor-adapter /> definition. 402 * 403 * @see android.content.ContentProvider 404 * @see android.widget.CursorAdapter 405 * @see #loadAdapter(android.content.Context, int, Object[]) 406 */ 407 public static CursorAdapter loadCursorAdapter(Context context, int id, String uri, 408 Object... parameters) { 409 410 XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, 411 parameters); 412 413 if (uri != null) { 414 adapter.setUri(uri); 415 } 416 adapter.load(); 417 418 return adapter; 419 } 420 421 /** 422 * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified 423 * XML resource. The content of the adapter is loaded from the specified cursor. 424 * You are responsible for managing the supplied cursor.</p> 425 * 426 * <p>The format of the XML definition of the cursor adapter is documented at 427 * the top of this page.</p> 428 * 429 * @param context The context to load the XML resource from. 430 * @param id The identifier of the XML resource declaring the adapter. 431 * @param cursor The cursor containing the data for the adapter. 432 * @param parameters Optional parameters to pass to the CursorAdapter, used 433 * to substitute values in the selection expression. 434 * 435 * @return A {@link android.widget.CursorAdapter} 436 * 437 * @throws IllegalArgumentException If the XML resource does not contain 438 * a valid <cursor-adapter /> definition. 439 * 440 * @see android.content.ContentProvider 441 * @see android.widget.CursorAdapter 442 * @see android.database.Cursor 443 * @see #loadAdapter(android.content.Context, int, Object[]) 444 */ 445 public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor, 446 Object... parameters) { 447 448 XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, 449 parameters); 450 451 if (cursor != null) { 452 adapter.changeCursor(cursor); 453 } 454 455 return adapter; 456 } 457 458 /** 459 * <p>Loads the adapter defined in the specified XML resource. The XML definition of 460 * the adapter must follow the format definition of one of the supported adapter 461 * types described at the top of this page.</p> 462 * 463 * <p><strong>Note:</strong> If the loaded adapter is a {@link android.widget.CursorAdapter} 464 * and the supplied {@link android.content.Context} is an {@link android.app.Activity}, 465 * the cursor returned by the content provider will be automatically managed. Otherwise, 466 * you are responsible for managing the cursor yourself.</p> 467 * 468 * @param context The context to load the XML resource from. 469 * @param id The identifier of the XML resource declaring the adapter. 470 * @param parameters Optional parameters to pass to the adapter. 471 * 472 * @return An adapter instance. 473 * 474 * @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[]) 475 * @see #loadCursorAdapter(android.content.Context, int, String, Object[]) 476 */ 477 public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) { 478 final BaseAdapter adapter = loadAdapter(context, id, null, parameters); 479 if (adapter instanceof ManagedAdapter) { 480 ((ManagedAdapter) adapter).load(); 481 } 482 return adapter; 483 } 484 485 /** 486 * Loads an adapter from the specified XML resource. The optional assertName can 487 * be used to exit early if the adapter defined in the XML resource is not of the 488 * expected type. 489 * 490 * @param context The context to associate with the adapter. 491 * @param id The resource id of the XML document defining the adapter. 492 * @param assertName The mandatory name of the adapter in the XML document. 493 * Ignored if null. 494 * @param parameters Optional parameters passed to the adapter. 495 * 496 * @return An instance of {@link android.widget.BaseAdapter}. 497 */ 498 private static BaseAdapter loadAdapter(Context context, int id, String assertName, 499 Object... parameters) { 500 501 XmlResourceParser parser = null; 502 try { 503 parser = context.getResources().getXml(id); 504 return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser), 505 id, parameters, assertName); 506 } catch (XmlPullParserException ex) { 507 Resources.NotFoundException rnf = new Resources.NotFoundException( 508 "Can't load adapter resource ID " + 509 context.getResources().getResourceEntryName(id)); 510 rnf.initCause(ex); 511 throw rnf; 512 } catch (IOException ex) { 513 Resources.NotFoundException rnf = new Resources.NotFoundException( 514 "Can't load adapter resource ID " + 515 context.getResources().getResourceEntryName(id)); 516 rnf.initCause(ex); 517 throw rnf; 518 } finally { 519 if (parser != null) parser.close(); 520 } 521 } 522 523 /** 524 * Generates an adapter using the specified XML parser. This method is responsible 525 * for choosing the type of the adapter to create based on the content of the 526 * XML parser. 527 * 528 * This method will generate an {@link IllegalArgumentException} if 529 * <code>assertName</code> is not null and does not match the root tag of the XML 530 * document. 531 */ 532 private static BaseAdapter createAdapterFromXml(Context c, 533 XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters, 534 String assertName) throws XmlPullParserException, IOException { 535 536 BaseAdapter adapter = null; 537 538 // Make sure we are on a start tag. 539 int type; 540 int depth = parser.getDepth(); 541 542 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && 543 type != XmlPullParser.END_DOCUMENT) { 544 545 if (type != XmlPullParser.START_TAG) { 546 continue; 547 } 548 549 String name = parser.getName(); 550 if (assertName != null && !assertName.equals(name)) { 551 throw new IllegalArgumentException("The adapter defined in " + 552 c.getResources().getResourceEntryName(id) + " must be a <" + 553 assertName + " />"); 554 } 555 556 if (ADAPTER_CURSOR.equals(name)) { 557 adapter = createCursorAdapter(c, parser, attrs, id, parameters); 558 } else { 559 throw new IllegalArgumentException("Unknown adapter name " + parser.getName() + 560 " in " + c.getResources().getResourceEntryName(id)); 561 } 562 } 563 564 return adapter; 565 566 } 567 568 /** 569 * Creates an XmlCursorAdapter using an XmlCursorAdapterParser. 570 */ 571 private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser, 572 AttributeSet attrs, int id, Object[] parameters) 573 throws IOException, XmlPullParserException { 574 575 return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters); 576 } 577 578 /** 579 * Parser that can generate XmlCursorAdapter instances. This parser is responsible for 580 * handling all the attributes and child nodes for a <cursor-adapter />. 581 */ 582 private static class XmlCursorAdapterParser { 583 private static final String ADAPTER_CURSOR_BIND = "bind"; 584 private static final String ADAPTER_CURSOR_SELECT = "select"; 585 private static final String ADAPTER_CURSOR_AS_STRING = "string"; 586 private static final String ADAPTER_CURSOR_AS_IMAGE = "image"; 587 private static final String ADAPTER_CURSOR_AS_TAG = "tag"; 588 private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri"; 589 private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable"; 590 private static final String ADAPTER_CURSOR_MAP = "map"; 591 private static final String ADAPTER_CURSOR_TRANSFORM = "transform"; 592 593 private final Context mContext; 594 private final XmlPullParser mParser; 595 private final AttributeSet mAttrs; 596 private final int mId; 597 598 private final HashMap<String, CursorBinder> mBinders; 599 private final ArrayList<String> mFrom; 600 private final ArrayList<Integer> mTo; 601 private final CursorTransformation mIdentity; 602 private final Resources mResources; 603 604 public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) { 605 mContext = c; 606 mParser = parser; 607 mAttrs = attrs; 608 mId = id; 609 610 mResources = mContext.getResources(); 611 mBinders = new HashMap<String, CursorBinder>(); 612 mFrom = new ArrayList<String>(); 613 mTo = new ArrayList<Integer>(); 614 mIdentity = new IdentityTransformation(mContext); 615 } 616 617 public XmlCursorAdapter parse(Object[] parameters) 618 throws IOException, XmlPullParserException { 619 620 Resources resources = mResources; 621 TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter); 622 623 String uri = a.getString(R.styleable.CursorAdapter_uri); 624 String selection = a.getString(R.styleable.CursorAdapter_selection); 625 String sortOrder = a.getString(R.styleable.CursorAdapter_sortOrder); 626 int layout = a.getResourceId(R.styleable.CursorAdapter_layout, 0); 627 if (layout == 0) { 628 throw new IllegalArgumentException("The layout specified in " + 629 resources.getResourceEntryName(mId) + " does not exist"); 630 } 631 632 a.recycle(); 633 634 XmlPullParser parser = mParser; 635 int type; 636 int depth = parser.getDepth(); 637 638 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && 639 type != XmlPullParser.END_DOCUMENT) { 640 641 if (type != XmlPullParser.START_TAG) { 642 continue; 643 } 644 645 String name = parser.getName(); 646 647 if (ADAPTER_CURSOR_BIND.equals(name)) { 648 parseBindTag(); 649 } else if (ADAPTER_CURSOR_SELECT.equals(name)) { 650 parseSelectTag(); 651 } else { 652 throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + 653 resources.getResourceEntryName(mId)); 654 } 655 } 656 657 String[] fromArray = mFrom.toArray(new String[mFrom.size()]); 658 int[] toArray = new int[mTo.size()]; 659 for (int i = 0; i < toArray.length; i++) { 660 toArray[i] = mTo.get(i); 661 } 662 663 String[] selectionArgs = null; 664 if (parameters != null) { 665 selectionArgs = new String[parameters.length]; 666 for (int i = 0; i < selectionArgs.length; i++) { 667 selectionArgs[i] = (String) parameters[i]; 668 } 669 } 670 671 return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection, 672 selectionArgs, sortOrder, mBinders); 673 } 674 675 private void parseSelectTag() { 676 TypedArray a = mResources.obtainAttributes(mAttrs, 677 R.styleable.CursorAdapter_SelectItem); 678 679 String fromName = a.getString(R.styleable.CursorAdapter_SelectItem_column); 680 if (fromName == null) { 681 throw new IllegalArgumentException("A select item in " + 682 mResources.getResourceEntryName(mId) + 683 " does not have a 'column' attribute"); 684 } 685 686 a.recycle(); 687 688 mFrom.add(fromName); 689 mTo.add(View.NO_ID); 690 } 691 692 private void parseBindTag() throws IOException, XmlPullParserException { 693 Resources resources = mResources; 694 TypedArray a = resources.obtainAttributes(mAttrs, 695 R.styleable.CursorAdapter_BindItem); 696 697 String fromName = a.getString(R.styleable.CursorAdapter_BindItem_from); 698 if (fromName == null) { 699 throw new IllegalArgumentException("A bind item in " + 700 resources.getResourceEntryName(mId) + " does not have a 'from' attribute"); 701 } 702 703 int toName = a.getResourceId(R.styleable.CursorAdapter_BindItem_to, 0); 704 if (toName == 0) { 705 throw new IllegalArgumentException("A bind item in " + 706 resources.getResourceEntryName(mId) + " does not have a 'to' attribute"); 707 } 708 709 String asType = a.getString(R.styleable.CursorAdapter_BindItem_as); 710 if (asType == null) { 711 throw new IllegalArgumentException("A bind item in " + 712 resources.getResourceEntryName(mId) + " does not have an 'as' attribute"); 713 } 714 715 mFrom.add(fromName); 716 mTo.add(toName); 717 mBinders.put(fromName, findBinder(asType)); 718 719 a.recycle(); 720 } 721 722 private CursorBinder findBinder(String type) throws IOException, XmlPullParserException { 723 final XmlPullParser parser = mParser; 724 final Context context = mContext; 725 CursorTransformation transformation = mIdentity; 726 727 int tagType; 728 int depth = parser.getDepth(); 729 730 final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type); 731 732 while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 733 && tagType != XmlPullParser.END_DOCUMENT) { 734 735 if (tagType != XmlPullParser.START_TAG) { 736 continue; 737 } 738 739 String name = parser.getName(); 740 741 if (ADAPTER_CURSOR_TRANSFORM.equals(name)) { 742 transformation = findTransformation(); 743 } else if (ADAPTER_CURSOR_MAP.equals(name)) { 744 if (!(transformation instanceof MapTransformation)) { 745 transformation = new MapTransformation(context); 746 } 747 findMap(((MapTransformation) transformation), isDrawable); 748 } else { 749 throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + 750 context.getResources().getResourceEntryName(mId)); 751 } 752 } 753 754 if (ADAPTER_CURSOR_AS_STRING.equals(type)) { 755 return new StringBinder(context, transformation); 756 } else if (ADAPTER_CURSOR_AS_TAG.equals(type)) { 757 return new TagBinder(context, transformation); 758 } else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) { 759 return new ImageBinder(context, transformation); 760 } else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) { 761 return new ImageUriBinder(context, transformation); 762 } else if (isDrawable) { 763 return new DrawableBinder(context, transformation); 764 } else { 765 return createBinder(type, transformation); 766 } 767 } 768 769 private CursorBinder createBinder(String type, CursorTransformation transformation) { 770 if (mContext.isRestricted()) return null; 771 772 try { 773 final Class<?> klass = Class.forName(type, true, mContext.getClassLoader()); 774 if (CursorBinder.class.isAssignableFrom(klass)) { 775 final Constructor<?> c = klass.getDeclaredConstructor( 776 Context.class, CursorTransformation.class); 777 return (CursorBinder) c.newInstance(mContext, transformation); 778 } 779 } catch (ClassNotFoundException e) { 780 throw new IllegalArgumentException("Cannot instanciate binder type in " + 781 mContext.getResources().getResourceEntryName(mId) + ": " + type, e); 782 } catch (NoSuchMethodException e) { 783 throw new IllegalArgumentException("Cannot instanciate binder type in " + 784 mContext.getResources().getResourceEntryName(mId) + ": " + type, e); 785 } catch (InvocationTargetException e) { 786 throw new IllegalArgumentException("Cannot instanciate binder type in " + 787 mContext.getResources().getResourceEntryName(mId) + ": " + type, e); 788 } catch (InstantiationException e) { 789 throw new IllegalArgumentException("Cannot instanciate binder type in " + 790 mContext.getResources().getResourceEntryName(mId) + ": " + type, e); 791 } catch (IllegalAccessException e) { 792 throw new IllegalArgumentException("Cannot instanciate binder type in " + 793 mContext.getResources().getResourceEntryName(mId) + ": " + type, e); 794 } 795 796 return null; 797 } 798 799 private void findMap(MapTransformation transformation, boolean drawable) { 800 Resources resources = mResources; 801 802 TypedArray a = resources.obtainAttributes(mAttrs, 803 R.styleable.CursorAdapter_MapItem); 804 805 String from = a.getString(R.styleable.CursorAdapter_MapItem_fromValue); 806 if (from == null) { 807 throw new IllegalArgumentException("A map item in " + 808 resources.getResourceEntryName(mId) + 809 " does not have a 'fromValue' attribute"); 810 } 811 812 if (!drawable) { 813 String to = a.getString(R.styleable.CursorAdapter_MapItem_toValue); 814 if (to == null) { 815 throw new IllegalArgumentException("A map item in " + 816 resources.getResourceEntryName(mId) + 817 " does not have a 'toValue' attribute"); 818 } 819 transformation.addStringMapping(from, to); 820 } else { 821 int to = a.getResourceId(R.styleable.CursorAdapter_MapItem_toValue, 0); 822 if (to == 0) { 823 throw new IllegalArgumentException("A map item in " + 824 resources.getResourceEntryName(mId) + 825 " does not have a 'toValue' attribute"); 826 } 827 transformation.addResourceMapping(from, to); 828 } 829 830 a.recycle(); 831 } 832 833 private CursorTransformation findTransformation() { 834 Resources resources = mResources; 835 CursorTransformation transformation = null; 836 TypedArray a = resources.obtainAttributes(mAttrs, 837 R.styleable.CursorAdapter_TransformItem); 838 839 String className = a.getString(R.styleable.CursorAdapter_TransformItem_withClass); 840 if (className == null) { 841 String expression = a.getString( 842 R.styleable.CursorAdapter_TransformItem_withExpression); 843 transformation = createExpressionTransformation(expression); 844 } else if (!mContext.isRestricted()) { 845 try { 846 final Class<?> klas = Class.forName(className, true, mContext.getClassLoader()); 847 if (CursorTransformation.class.isAssignableFrom(klas)) { 848 final Constructor<?> c = klas.getDeclaredConstructor(Context.class); 849 transformation = (CursorTransformation) c.newInstance(mContext); 850 } 851 } catch (ClassNotFoundException e) { 852 throw new IllegalArgumentException("Cannot instanciate transform type in " + 853 mContext.getResources().getResourceEntryName(mId) + ": " + className, e); 854 } catch (NoSuchMethodException e) { 855 throw new IllegalArgumentException("Cannot instanciate transform type in " + 856 mContext.getResources().getResourceEntryName(mId) + ": " + className, e); 857 } catch (InvocationTargetException e) { 858 throw new IllegalArgumentException("Cannot instanciate transform type in " + 859 mContext.getResources().getResourceEntryName(mId) + ": " + className, e); 860 } catch (InstantiationException e) { 861 throw new IllegalArgumentException("Cannot instanciate transform type in " + 862 mContext.getResources().getResourceEntryName(mId) + ": " + className, e); 863 } catch (IllegalAccessException e) { 864 throw new IllegalArgumentException("Cannot instanciate transform type in " + 865 mContext.getResources().getResourceEntryName(mId) + ": " + className, e); 866 } 867 } 868 869 a.recycle(); 870 871 if (transformation == null) { 872 throw new IllegalArgumentException("A transform item in " + 873 resources.getResourceEntryName(mId) + " must have a 'withClass' or " + 874 "'withExpression' attribute"); 875 } 876 877 return transformation; 878 } 879 880 private CursorTransformation createExpressionTransformation(String expression) { 881 return new ExpressionTransformation(mContext, expression); 882 } 883 } 884 885 /** 886 * Interface used by adapters that require to be loaded after creation. 887 */ 888 private static interface ManagedAdapter { 889 /** 890 * Loads the content of the adapter, asynchronously. 891 */ 892 void load(); 893 } 894 895 /** 896 * Implementation of a Cursor adapter defined in XML. This class is a thin wrapper 897 * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders. 898 */ 899 private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter { 900 private Context mContext; 901 private String mUri; 902 private final String mSelection; 903 private final String[] mSelectionArgs; 904 private final String mSortOrder; 905 private final int[] mTo; 906 private final String[] mFrom; 907 private final String[] mColumns; 908 private final CursorBinder[] mBinders; 909 private AsyncTask<Void,Void,Cursor> mLoadTask; 910 911 XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to, 912 String selection, String[] selectionArgs, String sortOrder, 913 HashMap<String, CursorBinder> binders) { 914 915 super(context, layout, null, from, to); 916 mContext = context; 917 mUri = uri; 918 mFrom = from; 919 mTo = to; 920 mSelection = selection; 921 mSelectionArgs = selectionArgs; 922 mSortOrder = sortOrder; 923 mColumns = new String[from.length + 1]; 924 // This is mandatory in CursorAdapter 925 mColumns[0] = "_id"; 926 System.arraycopy(from, 0, mColumns, 1, from.length); 927 928 CursorBinder basic = new StringBinder(context, new IdentityTransformation(context)); 929 final int count = from.length; 930 mBinders = new CursorBinder[count]; 931 932 for (int i = 0; i < count; i++) { 933 CursorBinder binder = binders.get(from[i]); 934 if (binder == null) binder = basic; 935 mBinders[i] = binder; 936 } 937 } 938 939 @Override 940 public void bindView(View view, Context context, Cursor cursor) { 941 final int count = mTo.length; 942 final int[] to = mTo; 943 final CursorBinder[] binders = mBinders; 944 945 for (int i = 0; i < count; i++) { 946 final View v = view.findViewById(to[i]); 947 if (v != null) { 948 // Not optimal, the column index could be cached 949 binders[i].bind(v, cursor, cursor.getColumnIndex(mFrom[i])); 950 } 951 } 952 } 953 954 public void load() { 955 if (mUri != null) { 956 mLoadTask = new QueryTask().execute(); 957 } 958 } 959 960 void setUri(String uri) { 961 mUri = uri; 962 } 963 964 @Override 965 public void changeCursor(Cursor c) { 966 if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) { 967 mLoadTask.cancel(true); 968 mLoadTask = null; 969 } 970 super.changeCursor(c); 971 } 972 973 class QueryTask extends AsyncTask<Void, Void, Cursor> { 974 @Override 975 protected Cursor doInBackground(Void... params) { 976 if (mContext instanceof Activity) { 977 return ((Activity) mContext).managedQuery( 978 Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); 979 } else { 980 return mContext.getContentResolver().query( 981 Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); 982 } 983 } 984 985 @Override 986 protected void onPostExecute(Cursor cursor) { 987 if (!isCancelled()) { 988 XmlCursorAdapter.super.changeCursor(cursor); 989 } 990 } 991 } 992 } 993 994 /** 995 * Identity transformation, returns the content of the specified column as a String, 996 * without performing any manipulation. This is used when no transformation is specified. 997 */ 998 private static class IdentityTransformation extends CursorTransformation { 999 public IdentityTransformation(Context context) { 1000 super(context); 1001 } 1002 1003 @Override 1004 public String transform(Cursor cursor, int columnIndex) { 1005 return cursor.getString(columnIndex); 1006 } 1007 } 1008 1009 /** 1010 * An expression transformation is a simple template based replacement utility. 1011 * In an expression, each segment of the form <code>{([^}]+)}</code> is replaced 1012 * with the value of the column of name $1. 1013 */ 1014 private static class ExpressionTransformation extends CursorTransformation { 1015 private final ExpressionNode mFirstNode = new ConstantExpressionNode(""); 1016 private final StringBuilder mBuilder = new StringBuilder(); 1017 1018 public ExpressionTransformation(Context context, String expression) { 1019 super(context); 1020 1021 parse(expression); 1022 } 1023 1024 private void parse(String expression) { 1025 ExpressionNode node = mFirstNode; 1026 int segmentStart; 1027 int count = expression.length(); 1028 1029 for (int i = 0; i < count; i++) { 1030 char c = expression.charAt(i); 1031 // Start a column name segment 1032 segmentStart = i; 1033 if (c == '{') { 1034 while (i < count && (c = expression.charAt(i)) != '}') { 1035 i++; 1036 } 1037 // We've reached the end, but the expression didn't close 1038 if (c != '}') { 1039 throw new IllegalStateException("The transform expression contains a " + 1040 "non-closed column name: " + 1041 expression.substring(segmentStart + 1, i)); 1042 } 1043 node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i)); 1044 } else { 1045 while (i < count && (c = expression.charAt(i)) != '{') { 1046 i++; 1047 } 1048 node.next = new ConstantExpressionNode(expression.substring(segmentStart, i)); 1049 // Rewind if we've reached a column expression 1050 if (c == '{') i--; 1051 } 1052 node = node.next; 1053 } 1054 } 1055 1056 @Override 1057 public String transform(Cursor cursor, int columnIndex) { 1058 final StringBuilder builder = mBuilder; 1059 builder.delete(0, builder.length()); 1060 1061 ExpressionNode node = mFirstNode; 1062 // Skip the first node 1063 while ((node = node.next) != null) { 1064 builder.append(node.asString(cursor)); 1065 } 1066 1067 return builder.toString(); 1068 } 1069 1070 static abstract class ExpressionNode { 1071 public ExpressionNode next; 1072 1073 public abstract String asString(Cursor cursor); 1074 } 1075 1076 static class ConstantExpressionNode extends ExpressionNode { 1077 private final String mConstant; 1078 1079 ConstantExpressionNode(String constant) { 1080 mConstant = constant; 1081 } 1082 1083 @Override 1084 public String asString(Cursor cursor) { 1085 return mConstant; 1086 } 1087 } 1088 1089 static class ColumnExpressionNode extends ExpressionNode { 1090 private final String mColumnName; 1091 private Cursor mSignature; 1092 private int mColumnIndex = -1; 1093 1094 ColumnExpressionNode(String columnName) { 1095 mColumnName = columnName; 1096 } 1097 1098 @Override 1099 public String asString(Cursor cursor) { 1100 if (cursor != mSignature || mColumnIndex == -1) { 1101 mColumnIndex = cursor.getColumnIndex(mColumnName); 1102 mSignature = cursor; 1103 } 1104 1105 return cursor.getString(mColumnIndex); 1106 } 1107 } 1108 } 1109 1110 /** 1111 * A map transformation offers a simple mapping between specified String values 1112 * to Strings or integers. 1113 */ 1114 private static class MapTransformation extends CursorTransformation { 1115 private final HashMap<String, String> mStringMappings; 1116 private final HashMap<String, Integer> mResourceMappings; 1117 1118 public MapTransformation(Context context) { 1119 super(context); 1120 mStringMappings = new HashMap<String, String>(); 1121 mResourceMappings = new HashMap<String, Integer>(); 1122 } 1123 1124 void addStringMapping(String from, String to) { 1125 mStringMappings.put(from, to); 1126 } 1127 1128 void addResourceMapping(String from, int to) { 1129 mResourceMappings.put(from, to); 1130 } 1131 1132 @Override 1133 public String transform(Cursor cursor, int columnIndex) { 1134 final String value = cursor.getString(columnIndex); 1135 final String transformed = mStringMappings.get(value); 1136 return transformed == null ? value : transformed; 1137 } 1138 1139 @Override 1140 public int transformToResource(Cursor cursor, int columnIndex) { 1141 final String value = cursor.getString(columnIndex); 1142 final Integer transformed = mResourceMappings.get(value); 1143 try { 1144 return transformed == null ? Integer.parseInt(value) : transformed; 1145 } catch (NumberFormatException e) { 1146 return 0; 1147 } 1148 } 1149 } 1150 1151 /** 1152 * Binds a String to a TextView. 1153 */ 1154 private static class StringBinder extends CursorBinder { 1155 public StringBinder(Context context, CursorTransformation transformation) { 1156 super(context, transformation); 1157 } 1158 1159 @Override 1160 public boolean bind(View view, Cursor cursor, int columnIndex) { 1161 if (view instanceof TextView) { 1162 final String text = mTransformation.transform(cursor, columnIndex); 1163 ((TextView) view).setText(text); 1164 return true; 1165 } 1166 return false; 1167 } 1168 } 1169 1170 /** 1171 * Binds an image blob to an ImageView. 1172 */ 1173 private static class ImageBinder extends CursorBinder { 1174 public ImageBinder(Context context, CursorTransformation transformation) { 1175 super(context, transformation); 1176 } 1177 1178 @Override 1179 public boolean bind(View view, Cursor cursor, int columnIndex) { 1180 if (view instanceof ImageView) { 1181 final byte[] data = cursor.getBlob(columnIndex); 1182 ((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0, 1183 data.length)); 1184 return true; 1185 } 1186 return false; 1187 } 1188 } 1189 1190 private static class TagBinder extends CursorBinder { 1191 public TagBinder(Context context, CursorTransformation transformation) { 1192 super(context, transformation); 1193 } 1194 1195 @Override 1196 public boolean bind(View view, Cursor cursor, int columnIndex) { 1197 final String text = mTransformation.transform(cursor, columnIndex); 1198 view.setTag(text); 1199 return true; 1200 } 1201 } 1202 1203 /** 1204 * Binds an image URI to an ImageView. 1205 */ 1206 private static class ImageUriBinder extends CursorBinder { 1207 public ImageUriBinder(Context context, CursorTransformation transformation) { 1208 super(context, transformation); 1209 } 1210 1211 @Override 1212 public boolean bind(View view, Cursor cursor, int columnIndex) { 1213 if (view instanceof ImageView) { 1214 ((ImageView) view).setImageURI(Uri.parse( 1215 mTransformation.transform(cursor, columnIndex))); 1216 return true; 1217 } 1218 return false; 1219 } 1220 } 1221 1222 /** 1223 * Binds a drawable resource identifier to an ImageView. 1224 */ 1225 private static class DrawableBinder extends CursorBinder { 1226 public DrawableBinder(Context context, CursorTransformation transformation) { 1227 super(context, transformation); 1228 } 1229 1230 @Override 1231 public boolean bind(View view, Cursor cursor, int columnIndex) { 1232 if (view instanceof ImageView) { 1233 final int resource = mTransformation.transformToResource(cursor, columnIndex); 1234 if (resource == 0) return false; 1235 1236 ((ImageView) view).setImageResource(resource); 1237 return true; 1238 } 1239 return false; 1240 } 1241 } 1242 } 1243