Chapter 27

Building a Content Provider

Building a content provider is probably the most complicated and tedious task in all of Android development. There are many requirements of a content provider, in terms of methods to implement and public data members to supply. And, until you try using your content provider, you have no great way of telling if you did it correctly (versus, say, building an activity and getting validation errors from the resource compiler).

That being said, building a content provider is of huge importance if your application wishes to make data available to other applications. If your application is keeping its data solely to itself, you may be able to avoid creating a content provider, and just access the data directly from your activities. But if you want your data to possibly be used by others—for example, you are building a feed reader and you want other programs to be able to access the feeds you are downloading and caching—then a content provider is right for you.

This chapter shows some sample bits of code from the ContentProvider/ConstantsPlus application. This is the same basic application as was first shown back in Chapter 22, but rewritten to pull the database logic into a content provider, which is then used by the activity.

First, Some Dissection

As discussed in the previous chapter, the content Uri is the linchpin behind accessing data inside a content provider. When using a content provider, all you really need to know is the provider's base Uri. From there, you can run queries as needed, or construct a Uri to a specific instance if you know the instance identifier.

However, when building a content provider, you need to know a bit more about the innards of the content Uri.

A content Uri has two to four pieces, depending on the situation:

  • It always has a scheme (content://), indicating it is a content Uri instead of a Uri to a web resource (http://).
  • It always has an authority, which is the first path segment after the scheme. The authority is a unique string identifying the content provider that handles the content associated with this Uri.
  • It may have a data type path, which is the list of path segments after the authority and before the instance identifier (if any). The data type path can be empty, if the content provider handles only one type of content. It can be a single path segment (foo) or a chain of path segments (foo/bar/goo) as needed to handle whatever data access scenarios the content provider requires.
  • It may have an instance identifier, which is an integer identifying a specific piece of content. A content Uri without an instance identifier refers to the collection of content represented by the authority (and, where provided, the data path).

For example, a content Uri could be as simple as content://sekrits, which would refer to the collection of content held by whatever content provider was tied to the sekrits authority (e.g., SecretsProvider). Or it could be as complex as content://sekrits/card/pin/17, which would refer to a piece of content (identified as 17) managed by the Sekrits content provider that is of the data type card/pin.

Next, Some Typing

Next, you need to come up with some MIME types corresponding with the content your content provider will provide.

Android uses both the content Uri and the MIME type as ways to identify content on the device. A collection content Uri—or, more accurately, the combination authority and data type path—should map to a pair of MIME types. One MIME type will represent the collection; the other will represent an instance. These map to the Uri patterns listed in the previous section for no-identifier and identifier, respectively. As you saw earlier in this book, you can fill a MIME type into an Intent to route the Intent to the proper activity (e.g., ACTION_PICK on a collection MIME type to call up a selection activity to pick an instance out of that collection).

The collection MIME type should be of the form vnd.X.cursor.dir/Y, where X is the name of your firm, organization, or project, and Y is a dot-delimited type name. So, for example, you might use vnd.tlagency.cursor.dir/sekrits.card.pin as the MIME type for your collection of secrets.

The instance MIME type should be of the form vnd.X.cursor.item/Y, usually for the same values of X and Y as you used for the collection MIME type (though that is not strictly required).

Creating Your Content Provider

Creating a content provider involves four basic steps: create a provider class, supply a Uri, declare the properties, and update the manifest.

Step 1: Create a Provider Class

Just as an activity and intent receiver are both Java classes, so is a content provider. So, the big step in creating a content provider is crafting its Java class, with a base class of ContentProvider.

In your subclass of ContentProvider, you are responsible for implementing six methods that, when combined, perform the services that a content provider is supposed to offer to activities wishing to create, read, update, or delete content.

onCreate()

As with an activity, the main entry point to a content provider is onCreate(). Here, you can do whatever initialization you want. In particular, here is where you should lazy-initialize your data store. For example, if you plan on storing your data in such-and-so directory on an SD card, with an XML file serving as a table of contents, you should check and see if that directory and XML file are there; if not, create them so the rest of your content provider knows they are out there and available for use.

Similarly, if you have rewritten your content provider sufficiently to cause the data store to shift structure, you should check to see what structure you have now and adjust it if what you have is out of date. You don't write your own “installer” program. This means that you have no great way of determining if, when onCreate() is called, this is the first time ever for the content provider, the first time for a new release of a content provider that was upgraded in place, or just a normal startup.

For example, here is the onCreate() method for Provider, from the ContentProvider/ConstantsPlus sample application:

@Override
public boolean
onCreate() {
  db=(new
DatabaseHelper(getContext())).getWritableDatabase();

  return (db == null) ? false : true;
}

While that doesn't seem all that special, the “magic” is in the private DatabaseHelper object, described in Chapter 22.

query()

As you might expect, the query() method is where your content provider gets details on a query some activity wants to perform. It is up to you to actually process said query.

The query method gets the following as parameters:

  • A Uri representing the collection or instance being queried
  • A String[] representing the list of properties that should be returned
  • A String representing what amounts to a SQL WHERE clause, constraining which instances should be considered for the query results
  • A String[] representing values to go in the WHERE clause, replacing any ? character found there
  • A String representing what amounts to a SQL ORDER BY clause

You are responsible for interpreting these parameters however they make sense, and returning a Cursor that can be used to iterate over and access the data.

As you can imagine, these parameters are aimed toward people using a SQLite database for storage. You are welcome to ignore some of these parameters (e.g., you can elect not to try to roll your own SQL WHERE clause parser), but you need to document that fact so activities attempt to query you only by instance Uri, and not by using parameters you choose not to handle.

For SQLite-backed storage providers, however, the query() method implementation should be largely boilerplate. Use a SQLiteQueryBuilder to convert the various parameters into a single SQL statement, and then use query() on the builder to actually invoke the query and give you back a Cursor. The Cursor is what your query() method returns.

For example, here is query() from Provider:

@Override
public Cursor
query(Uri url, String[] projection, String selection,
                    String[] selectionArgs, String sort) {
  SQLiteQueryBuilder qb=new
SQLiteQueryBuilder();

  qb.
setTables( getTableName());

  if (
isCollectionUri(url)) {
    qb.
setProjectionMap( getDefaultProjection());
  }
  else {
    qb.
appendWhere( getIdColumnName()+"="+url. getPathSegments(). get(1));
  }

  String orderBy;

  if (TextUtils.
isEmpty(sort)) {
    orderBy=
getDefaultSortOrder();
  } else {
    orderBy=sort;
  }

  Cursor c=qb.
query(db, projection, selection, selectionArgs,
                    null, null, orderBy);
  c.
setNotificationUri( getContext(). getContentResolver(), url);
  return c;
}

We create a SQLiteQueryBuilder and pour the query details into the builder. Note that the query could be based around either a collection or an instance Uri. In the latter case, we need to add the instance ID to the query. When done, we use the query() method on the builder to get a Cursor for the results.

insert()

Your insert() method will receive a Uri representing the collection and a ContentValues structure with the initial data for the new instance. You are responsible for creating the new instance, filling in the supplied data, and returning a Uri to the new instance.

If this is a SQLite-backed content provider, once again, the implementation is mostly boilerplate. You just need to validate that all required values were supplied by the activity, merge your own notion of default values with the supplied data, and call insert() on the database to actually create the instance.

For example, here is insert() from Provider:

@Override
public Uri
insert(Uri url, ContentValues initialValues) {
  long rowID;
  ContentValues values;

  if (initialValues!=null) {
    values=new
ContentValues(initialValues);
  } else {
    values=new
ContentValues();
  }

  if (!
isCollectionUri(url)) {
    throw new
IllegalArgumentException("Unknown URL " + url);
  }

  for (String colName :
getRequiredColumns()) {
    if (values.
containsKey(colName) == false) {
      throw new
IllegalArgumentException("Missing column: "+colName);
    }
  }

  
populateDefaultValues(values);

  rowID=db.
insert( getTableName(), getNullColumnHack(), values);
  if (rowID > 0) {
    Uri uri=ContentUris.
withAppendedId( getContentUri(), rowID);
    
getContext(). getContentResolver(). notifyChange(uri, null);
    return uri;
  }

  throw new
SQLException("Failed to insert row into " + url);
}

The pattern is the same as before: use the provider particulars plus the data to be inserted to actually do the insertion. Note the following regarding the example:

  • You can insert only into a collection Uri, so we validate that by calling isCollectionUri().
  • The provider also knows which columns are required (getRequiredColumns()), so we iterate over those and confirm our supplied values cover the requirements.
  • The provider is also responsible for filling in any default values (populateDefaultValues()) for columns not supplied in the insert() call and not automatically handled by the SQLite table definition.
update()

Your update() method gets the Uri of the instance or collection to change, a ContentValues structure with the new values to apply, a String for a SQL WHERE clause, and a String[] with parameters to use to replace ? characters found in the WHERE clause. Your responsibility is to identify the instance(s) to be modified (based on the Uri and WHERE clause), and then replace those instances' current property values with the ones supplied.

This will be annoying, unless you're using SQLite for storage. Then you can pretty much pass all the parameters you received to the update() call to the database, although the update() call will vary slightly depending on whether you are updating one instance or several instances.

For example, here is update() from Provider:

@Override
public int
update(Uri url, ContentValues values, String where, String[] whereArgs) {
  int count;

  if (
isCollectionUri(url)) {
    count=db.
update( getTableName(), values, where, whereArgs);
  }
  else {
    String segment=url.
getPathSegments(). get(1);
    count=db
        .
update( getTableName(), values, getIdColumnName()+"="
            + segment
            + (!TextUtils.
isEmpty(where) ? " AND (" + where
                + ')' : ""), whereArgs);
  }

  
getContext(). getContentResolver(). notifyChange(url, null);
  return count;
}

In this case, updates can either be to a specific instance or applied across the entire collection, so we check the Uri (isCollectionUri()) and, if it is an update for the collection, just perform the update. If we are updating a single instance, we need to add a constraint to the WHERE clause to update only for the requested row.

delete()

As with update(), delete() receives a Uri representing the instance or collection to work with and a WHERE clause and parameters. If the activity is deleting a single instance, the Uri should represent that instance and the WHERE clause may be null. But the activity might be requesting to delete an open-ended set of instances, using the WHERE clause to constrain which ones to delete.

As with update(), this is simple if you are using SQLite for database storage (sense a theme?). You can let it handle the idiosyncrasies of parsing and applying the WHERE clause. All you need to do is call delete() on the database.

For example, here is delete() from Provider:

@Override
public int
delete(Uri url, String where, String[] whereArgs) {
  int count;
  long rowId=0;

  if (
isCollectionUri(url)) {
    count=db.
delete( getTableName(), where, whereArgs);
  }
  else {
    String segment=url.
getPathSegments(). get(1);
    rowId=Long.
parseLong(segment);
    count=db
        .
delete( getTableName(), getIdColumnName()+"="
            + segment
            + (!TextUtils.
isEmpty(where) ? " AND (" + where
                + ')' : ""), whereArgs);
  }

  
getContext(). getContentResolver(). notifyChange(url, null);
  return count;
}

This is almost a clone of the update() implementation described in the preceding section, It either deletes a subset of the entire collection or deletes a single instance (if it also satisfies the supplied WHERE clause).

getType()

The last method you need to implement is getType(). This takes a Uri and returns the MIME type associated with that Uri. The Uri could be a collection or an instance Uri; you need to determine which was provided and return the corresponding MIME type.

For example, here is getType() from Provider:

@Override
public String
getType(Uri url) {
  if (
isCollectionUri(url)) {
    return(
getCollectionType());
  }

  return(
getSingleType());
}

As you can see, most of the logic delegates to private getCollectionType() and getSingleType() methods:

private String getCollectionType() {
  return("vnd.android.cursor.dir/vnd.commonsware.constant");
}

private String
getSingleType() {
  return("vnd.android.cursor.item/vnd.commonsware.constant");
}

Step 2: Supply a Uri

You also need to add a public static member—somewhere—containing the Uri for each collection your content provider supports. Typically, this is a public static final Uri put on the content provider class itself:

public static final Uri CONTENT_URI
    =Uri.
parse("content://com.commonsware.android.constants.Provider/constants");

You may wish to use the same namespace for the content Uri that you use for your Java classes, to reduce the chance of collision with others.

Step 3: Declare the Properties

Remember those properties you referenced when you were using a content provider, in the previous chapter? Well, you also need to have those for your own content provider.

Specifically, you want a public static class implementing BaseColumns that contains your property names, such as this example from Provider:

public static final class Constants implements BaseColumns {
  public static final Uri CONTENT_URI
      =Uri.
parse("content://com.commonsware.android.constants.Provider/constants");
  public static final String DEFAULT_SORT_ORDER="title";
  public static final String TITLE="title";
  public static final String VALUE="value";
}

If you are using SQLite as a data store, the values for the property name constants should be the corresponding column names in the table, so you can just pass the projection (array of properties) to SQLite on a query(), or pass the ContentValues on an insert() or update().

Note that nothing in here stipulates the types of the properties. They could be strings, integers, or whatever. The biggest limitation is what a Cursor can provide access to via its property getters. The fact that there is nothing in code that enforces type safety means you should document the property types well, so people attempting to use your content provider know what they can expect.

Step 4: Update the Manifest

The glue tying the content provider implementation to the rest of your application resides in your AndroidManifest.xml file. Simply add a <provider> element as a child of the <application> element:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.commonsware.android.constants">
  <application android:label="@string/app_name"
    android:icon="@drawable/cw">
    <provider android:name=".Provider"
            android:authorities="com.commonsware.android.constants.Provider" />
    <activity android:name=".ConstantsBrowser" android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
</manifest>

The android:name property is the name of the content provider class, with a leading dot to indicate it is in the stock namespace for this application's classes (just like you use with activities).

The android:authorities property should be a semicolon-delimited list of the authority values supported by the content provider. As discussed earlier in this chapter, each content Uri is made up of a scheme, authority, data type path, and instance identifier. Each authority from each CONTENT_URI value should be included in the android:authorities list.

Now, when Android encounters a content Uri, it can sift through the providers registered through manifests to find a matching authority. That tells Android which application and class implement the content provider, and from there, Android can bridge between the calling activity and the content provider being called.

Notify-on-Change Support

An optional feature your content provider can offer to its clients is notify-on-change support. This means that your content provider will let clients know if the data for a given content Uri changes.

For example, suppose you have created a content provider that retrieves RSS and Atom feeds from the Internet based on the user's feed subscriptions (via OPML, perhaps). The content provider offers read-only access to the contents of the feeds, with an eye toward several applications on the phone using those feeds versus everyone implementing their own feed poll-fetch-and-cache system. You have also implemented a service that will get updates to those feeds asynchronously, updating the underlying data store. Your content provider could alert applications using the feeds that such-and-so feed was updated, so applications using that specific feed can refresh and get the latest data.

On the content provider side, to do this, call notifyChange() on your ContentResolver instance (available in your content provider via getContext().getContentResolver()). This takes two parameters: the Uri of the piece of content that changed and the ContentObserver that initiated the change. In many cases, the latter will be null; a non-null value simply means that the observer that initiated the change will not be notified of its own changes.

On the content consumer side, an activity can call registerContentObserver() on its ContentResolver (via getContentResolver()). This ties a ContentObserver instance to a supplied Uri, and the observer will be notified whenever notifyChange() is called for that specific Uri. When the consumer is done with the Uri, unregisterContentObserver() releases the connection.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.76.12