Implementing a content provider

The Android SDK contains a document that describes nine steps to creating a content provider. In summary, they are:

1. Extend the ContentProvider class.

2. Define the CONTENT_URI for your content provider.

3. Create the data storage for your content.

4. Create the column names for communication with clients.

5. Define the process by which binary data is returned to the client.

6. Declare public static Strings that clients use to specify columns.

7. Implement the CRUD methods of a Cursor to return to the client.

8. Update the AndroidManifest.xml file to declare your <provider>.

9. Define MIME types for any new data types.

In the following sections, we'll examine each step in detail using the NotePad application as our guide.

Extend ContentProvider. Within NotePadProvider.java, the NotePadProvider class extends ContentProvider, as shown here:

public class NotePadProvider extends ContentProvider

Classes that extend ContentProvider must provide implementations for the following methods:

onCreate

This method is called during the content provider's startup. Any code you want to run just once, such as making a database connection, should reside in this method.

getType

This method, when given a URI, returns the MIME type of the data that this content provider provides at that URI. The URI comes from the client application interested in accessing the data. insert

This method is called when the client code wishes to insert data into the database your content provider is serving. Normally, the implementation for this method will either directly or indirectly result in a database insert operation.

query

This method is called whenever a client wishes to read data from the content provider's database. It is normally called through ContentProvider's managedQuery method. Normally, here you retrieve data using a SQL SELECT statement and return a cursor containing the requested data. update

This method is called when a client wishes to update one or more rows in the ContentProvider's database. It translates to a SQL UPDATE statement.

delete

This method is called when a client wishes to delete one or more rows in the ContentProvider's database. It translates to a SQL DELETE statement.

NotePadProvider class and instance variables. As usual, it's best to understand the major class and instance variables used by a method before examining how the method works. The variables we need to understand for the NotePad's ContentProvider class are:

private static final String DATABASE_NAME = "note_pad.db"; private static final int DATABASE_VERSION = 2; private static final String NOTES_TABLE_NAME = "notes"; private DatabaseHelper mOpenHelper;

DATABASE_NAME

The name of the database file on the device. For the NotePad project, the full path to the file is /data/data/com.example.android.notepad/databases/note_pad.db.

DATABASE_VERSION

The version of the database this code works with. If this number is higher than the version of the database itself, the application calls the DatabaseHelper.onUpdate method. See "Create the data storage" on page 122 for more information. NOTES_TABLE_NAME

The name of the notes table within the notes database.

mOpenHelper

This instance variable is initialized during onCreate. It provides access to the database for the insert, query, update, and delete methods.

In addition to these class and instance variables, the NotePadContentProvider class also has a static initialization block that performs complex initializations of static variables that can't be performed as simple one-liners:

private static HashMap<String, String> sNotesProjectionMap; private static final UriMatcher sUriMatcher; private static final int NOTES = 1; private static final int NOTE_ID = 2;

static {

sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES); sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);

sNotesProjectionMap = new HashMap<String, String>(); sNotesProjectionMap.put(Notes._ID, Notes._ID); sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE); sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE); sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE); sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);

The meanings of these variables follow:

sNotesProjectionMap

The projection map used by the query method. This HashMap maps the content provider's column names to database column names. A projection map is not required, but when used it must list all column names that might be returned by the query. In NotePadContentProvider, the content provider column names and the database column names are identical, so the sNotesProjectionMap is not required.

sUriMatcher

This data structure is loaded with several URI templates that match URIs clients can send the content provider. Each URI template is paired with an integer that the sUriMatcher returns when it's passed a matching URI. The integers are used as cases of a switch in other parts of the class. NotePadContentProvider has two types of URIs, represented by the NOTES and NOTES_ID integers. NOTES

sUriMatcher returns this value for note URIs that do not include a note ID.

NOTES_ID

sUriMatcher returns this value when the notes URI includes a note ID.

Define CONTENT_URI. When a client application uses a content resolver to request data, a URI that identifies the desired data is passed to the content resolver. Android tries to match the URI with the CONTENT_URI of each content provider it knows about to find the right provider for the client. Thus, the CONTENT_URI defines the type of URIs your content provider can process.

A CONTENT_URI consists of these parts:

content://

This initial string tells the Android framework that it must find a content provider to resolve the URI.

The authority

This string uniquely identifies the content provider and consists of up to two sections: the organizational section and the provider identifier section. The organizational section uniquely identifies the organization that created the content provider. The provider identifier section identifies a particular content provider that the organization created. For content providers that are built into Android, the organizational section is omitted. For instance, the built-in "media" authority that returns one or more images does not have the organizational section of the authority. However any content providers that are created by developers outside of Google's Android team must define both sections of the content provider. Thus, the Notepad example application's authority is com.google.provider.NotePad. The organizational section is com.google.provider, and the provider identifier section is NotePad. The Google documentation suggests that the best solution for picking the authority section of your CONTENT_URI is to use the fully qualified class name of the class implementing the content provider.

The authority section uniquely identifies the particular content provider that Android will call to respond to queries that it handles.

The path

The content provider can interpret the rest of the URI however it wants, but it must adhere to some requirements:

• If the content provider can return multiple data types, the URI must be constructed so that some part of the path specifies the type of data to return.

For instance, the built-in "Contacts" content provider provides many different types of data: People, Phones, ContactMethods, etc. The Contacts content provider uses strings in the URI to differentiate which type of data the user is requesting. Thus, to request a specific person, the URI will be something like this:

content://contacts/people/1

To request a specific phone number, the URI could be something like this:

content://contacts/people/1/phone/3

In the first case, the MIME data type returned will be vnd.android.cursor.item/ person, whereas in the second case, it will be vnd.android.cursor.item/phone. • The content provider must be capable of returning either one item or a set of item identifiers. The content provider will return a single item when an item identifier appears in the final portion of the URI. Looking back at our previous example, the URI content://contacts/people/1/phone/3 returned a single phone number of type vnd.android.cursor.item/phone. If the URI had instead been content://contacts/people/1/phone, the application would have returned a list of all of the phone numbers for the person having the person identifier number 1, and the MIME type of the data returned would be vnd.android.cursor.dir/ phone .

As mentioned earlier, the content provider can interpret the path portion of the URI however it wants. This means that it can use items in the path to filter data to return to the caller. For instance, the built-in "media" content provider can return either internal or external data, depending on whether the URI contains the word "internal" or "external" in the path.

The full CONTENT_URI for NotePad is content://com.google.provider.NotePad/notes.

The CONTENT_URI must be of type public static final Uri. It is defined in the NotePad class of the NotePad application. First, a string named AUTHORITY is defined:

public final class NotePad {

public static final String AUTHORITY = "com.google.provider.NotePad";

Then, the CONTENT_URI itself is defined:

public static final class Notes implements BaseColumns {

public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");

Create the data storage. A content provider can store data in any way it chooses. Because content providers use database semantics, the SQLite database is most commonly used. The onCreate method of the ContentProvider class (NotePadProvider in the NotePad application) creates this data store. The method is called during the content provider's initialization. In the NotePad application, the onCreate method creates a connection to the database, creating the database first if it does not exist. @Override public boolean onCreate() {

mOpenHelper = new DatabaseHelper(getContext()); return true;

private static class DatabaseHelper extends SQLiteOpenHelper {

DatabaseHelper(Context context) {

super(context, DATABASE_NAME, null, DATABASE_VERSION);

@Override public void onCreate(SQLiteDatabase db) {

db.execSQL("CREATE TABLE " + NOTES_TABLE_NAME + " (" + Notes._ID + " INTEGER PRIMARY KEY," + Notes.TITLE + " TEXT," + Notes.NOTE + " TEXT," + Notes.CREATED_DATE + " INTEGER,"

+ Notes.MODIFIED_DATE + " INTEGER" + ");");

^Override public void onUpgrade(SQLiteDatabase db, int oldver, int newver) { // destroy the old version -- not nice to do in a real app! db.execSQL("DROP TABLE IF EXISTS notes"); onCreate(db);

Here are some of the highlights of the code:

O Creates a new object of the DatabaseHelper class, which is derived from SQLiteOpen Helper. The constructor for DatabaseHelper knows to call onCreate or onUpgrade if it has to create or upgrade the database.

This is standard database code for Android, very similar to the database creation code from the MJAndroid project. A handle for the new DatabaseHelper class is assigned to the mOpenHelper class variable, which is used by the rest of the content provider to manipulate the database.

This method embeds raw SQL into a call to execSQL. As we'll see, further calls don't need to use SQL; instead, their simple CRUD operations use calls provided by the framework.

Data Store for Binary Data

The Android SDK documentation suggests that when your content provider stores binary data, such as a bitmap or music clip, the data should be stored outside of the database in a file, and the content provider should store a content:// URI in the database that points to the file. Client applications will query your content provider to retrieve that content:// URI and then retrieve the actual byte stream from the file it specifies.

The reason for this circuitous route is easy to understand after some examination. Because filesystem I/O is much faster and more versatile than dealing with SQLite blobs, it's better to use the Unix filesystem instead of SQL blobs. But since an Android application cannot read or write files that another application has created, a content provider must be used to access the blobs. Therefore, when the first content provider returns a pointer to a file containing a blob, that pointer must be in the form of a content:// URI instead of a Unix filename. The use of a content:// URI causes the file to be opened and read under the permissions of the content provider that owns the file, not the client application (which does not have access rights to the file).

To implement the file approach, instead of creating a hypothetical user table like this:

CREATE TABLE user (

_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, password TEXT, picture BLOB

the documentation suggests two tables that look like this:

CREATE TABLE user (

_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, password TEXT, picture TEXT

CREATE TABLE userPicture (

_id INTEGER PRIMARY KEY AUTOINCREMENT, _data TEXT

The picture column of the user table will store a content:// URI that points to a row in the userPicture table. The _data column of the userPicture table will point to a real file on the Android filesystem.

If the path to the file were stored directly in the user table, clients would get a path but be unable to open the file, because it's owned by the application serving up the content provider and the clients don't have permission to read it. In the solution shown here, however, access is controlled by a ContentResolver class, which we'll examine later.

The ContentResolver class looks for a column named _data when processing requests. If the file specified in that column is found, the class's openOutputStream method opens the file and returns a java.io.OutputStream to the client. This is the same object that would be returned if the client were able to open the file directly. The ContentResolver class is part of the same application as the content provider, and therefore is able to open the file when the client cannot.

Create the column names. Content providers exchange data with their clients in much the same way an SQL database exchanges data with database applications: using Cursors full of rows and columns of data. A content provider must define the column names it supports, just as a database application must define the columns it supports. When the content provider uses an SQLite database as its data store, the obvious solution is to give the content provider columns the same name as the database columns, and that's just what NotePadProvider does. Because of this, there is no mapping necessary between the NotePadProvider columns and the underlying database columns.

Not all applications make all of their data available to content provider clients, and some more complex applications may want to make derivative views available to content provider clients. The projection map described in "NotePadProvider class and instance variables" on page 119 is available to handle these complexities.

Supporting binary data. We already explained the recommended data structure for serving binary data in the sidebar "Data Store for Binary Data" on page 123. The other piece of the solution lies in the ContentResolver class, discussed later.

Declare column specification strings. The NotePadProvider columns are defined in the NotePad.Notes class, as mentioned in "NotePadProvider class and instance variables" on page 119. Every content provider must define an _id column to hold the record number of each row. The value of each _id must be unique within the content provider; it is the number that a client will append to the content provider's vnd.android.cursor.item URI when attempting to query for a single record.

When the content provider is backed by an SQLite database, as is the case for NotePadProvider, the _id should have the type INTEGER PRIMARY KEY AUTOINCREMENT. This way, the rows will have a unique _id number and _id numbers will not be reused, even when rows are deleted. This helps support referential integrity by ensuring that each new row has an _id that has never been used before. If row _ids are reused, there is a chance that cached URIs could point to the wrong data.

Implement the Cursor. A content provider implementation must override the CRUD methods of the ContentProvider base class: insert, query, update, and delete. For the NotePad application, these methods are defined in the NotePadProvider class.

Create data (insert). Classes that extend ContentProvider must override its insert method. This method receives values from a client, validates them, and then adds a new row to the database containing those values. The values are passed to the ContentProvider class in a ContentValues object: @Override public Uri insert(Uri uri, ContentValues initialValues) { // Validate the requested uri if (sUriMatcher.match(uri) != NOTES) {

throw new IllegalArgumentException("Unknown URI " + uri);

ContentValues values; if (initialValues != null)

values = new ContentValues(initialValues);

else values = new ContentValues();

Long now = Long.valueOf(System.currentTimeMillis());

// Make sure that the fields are all set if (values.containsKey(NotePad.Notes.CREATED_DATE) == false) values.put(NotePad.Notes.CREATED_DATE, now);

if (values.containsKey(NotePad.Notes.MODIFIED_DATE) == false) values.put(NotePad.Notes.MODIFIED_DATE, now);

if (values.containsKey(NotePad.Notes.TITLE) == false) { Resources r = Resources.getSystem();

values.put(NotePad.Notes.TITLE,r.getString(android.R.string.untitled));

if (values.containsKey(NotePad.Notes.NOTE) == false) { values.put(NotePad.Notes.NOTE, "");

SQLiteDatabase db = mOpenHelper.getWritableDatabase(); long rowId = db.insert(NOTES_TABLE_NAME, Notes.NOTE, values); if (rowId > 0) {

Uri noteUri=ContentUris.withAppendedId(NotePad.Notes.CONTENT_URI,rowId); getContext().getContentResolver().notifyChange(noteUri, null); return noteUri;

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

Read/select data (query). NotePadProvider must override the query method and return a Cursor containing the data requested. It starts by creating an instance of the SQLite QueryBuilder class, using both static information from the class and dynamic information from the URI. It then creates the Cursor directly from the database using the SQLiteQueryBuilder query. Finally, it returns the Cursor that the database created.

When the URI contains a note identification number, the NOTE_ID case is used. In this case, text is added to the WHERE clause so that only the note identified by the URI is included in the Cursor returned to the NotePadProvider client: @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

switch (sUriMatcher.match(uri)) { case NOTES:

qb.setTables(NOTES_TABLE_NAME);

qb.setProjectionMap(sNotesProjectionMap);

break;

case NOTE_ID:

qb.setTables(NOTES_TABLE_NAME); qb.setProjectionMap(sNotesProjectionMap);

qb.appendWhere(Notes._ID + "=" + uri.getPathSegments().get(1)); break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

// If no sort order is specified use the default String orderBy;

if (TextUtils.isEmpty(sortOrder)) {

orderBy = NotePad.Notes.DEFAULT_SORT_ORDER; } else {

orderBy = sortOrder;

// Get the database and run the query SQLiteDatabase db = mOpenHelper.getReadableDatabase();

Cursor c=qb.query(db,projection,selection,selectionArgs,null,null,orderBy);

// Tell cursor what uri to watch, so it knows when its source data changes c.setNotificationUri(getContext().getContentResolver(), uri); return c;

Update data. NotePadProvider's update method receives values from a client, validates them, and modifies relevant rows in the database given those values. It all boils down to the SQLiteDatabase's update method. The first value passed to update is the table name. This constant is defined elsewhere in the class. The second parameter, values, is a ContentValues object formed by the client of the ContentProvider. The final two arguments, where and whereArgs, are used to form the WHERE clause of the SQL UPDATE command.

The ContentValues object is created by the ContentProvider's client. It contains a map of database column names to new column values that is passed through to the SQLiteDatabase's update method.

The where string and the whereArgs string array work together to build the WHERE clause of the SQLite UPDATE command. This WHERE clause limits the scope of the UPDATE command to the rows that match its criteria. The where string can be built either to contain all of the information necessary to build the WHERE clause, or to contain a template that is filled out at runtime by inserting strings from the whereArgs string. The easiest way to understand this is with a couple of examples.

Let's suppose that you want to update only those rows where the dogName column is equal to 'Jackson'. As the content provider's client, you could create a single where string consisting of "dogName='Jackson'" and pass it along to the update method. This works well and is what many applications do. But unless you check your input very well, this method is subject to an SQL injection attack, as described earlier in the chapter.

The better approach is to pass a template as the where clause, something like "dogName=?". The question mark marks the location for the value of dogName, and the actual value is found in the whereArgs string array. The first question mark is replaced by the first value in the whereArgs string array. If there were a second question mark, it would be replaced with the second value, and so forth: ^Override public int update(Uri uri,ContentValues values,String where,String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count;

switch (sUriMatcher.match(uri)) { case NOTES:

count = db.update(NOTES_TABLE_NAME, values, where, whereArgs); break;

case NOTE_ID:

String noteId = uri.getPathSegments().get(1);

count = db.update(NOTES_TABLE_NAME, values, Notes._ID + "=" + noteId + (!TextUtils.isEmpty(where)?" AND ("+where+')':""), whereArgs);

break; default:

throw new IllegalArgumentException("Unknown URI " + uri);

getContext().getContentResolver().notifyChange(uri, null); return count;

Delete data. NotePadProvider's delete method is very similar to the update method, but instead of updating the rows with new data, it simply deletes them: @Override public int delete(Uri uri, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count;

switch (sUriMatcher.match(uri)) { case NOTES:

count = db.delete(NOTES_TABLE_NAME, where, whereArgs); break;

case NOTE_ID:

String noteId = uri.getPathSegments().get(1);

count = db.delete(NOTES_TABLE_NAME, Notes._ID + "=" + noteId

+ (!TextUtils.isEmpty(where)?" AND ("+where+')':""), whereArgs); break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

getContext().getContentResolver().notifyChange(uri, null); return count;

Updating AndroidManifest.xml. The AndroidManifest.xml file defines all external access to the application, including any content providers. Within the file, the <provider> tag declares the content provider.

The AndroidManifest.xml file within the NotePad project has the following <provider> tag:

<provider android:name="NotePadProvider"

android:authorities="com.google.provider.NotePad"

An android:authorities attribute must be defined within the <provider> tag. Android uses this attribute to identify the URIs that this content provider will fulfill.

The android:name tag is also required, and identifies the name of the content provider class. Note that this string matches the AUTHORITY string in the NotePad class, discussed earlier.

In sum, this section of the AndroidManifest.xml file can be translated to the following English statement: "This content provider accepts URIs that start with content:// com.google.provider.notepad/ and passes them to the NotePadProvider class."

Define MIME types. Your content provider must override the getType method. This method accepts a URI and returns the MIME type that corresponds to that URI. For the NotePadProvider, two types of URIs are accepted, so two types of URIs are returned:

• The content://com.google.provider.NotePad/notes URI will return a directory of zero or more notes, using the vnd.android.cursor.dir/vnd.google.note MIME type.

• A URI with an appended ID, of the form content://com.google.provider.NotePad/ notes/N, will return a single note, using the vnd.android.cursor.item/ vnd.google.note MIME type.

The client passes a URI to the Android framework to indicate the database it wants to access, and the Android framework calls your getType method internally to get the MIME type of the data. That helps Android decide what to do with the data returned by the content provider.

Your getType method must return the MIME type of the data at the given URI. In NotePad, the MIME types are stored as simple string variables, shown earlier in "Structure of the source code" on page 117. The return value starts with vnd.android.cursor.item for a single record and vnd.android.cursor.dir for multiple items:

@Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case NOTES:

return Notes.CONTENT_TYPE; // vnd.android.cursor.dir/vnd.google.note case NOTE_ID:

return Notes.CONTENT_ITEM_TYPE; // vnd.android.cursor.item/vnd.google.note default:

throw new IllegalArgumentException("Unknown URI " + uri);

0 0

Responses

  • REBECCA HANNAN
    When the gettype() method of content provider will called?
    7 years ago

Post a comment