Previous section   Next section

Mobile Phone Handheld Hardware Hardware Rick Rogers John Lombardo O'Reilly Media, Inc. O'Reilly Media Android Application Development, 1st Edition

8.2. Content Providers

Much of the time, an application's data is tightly bound to that application. For instance, a book reader application will typically have one datafile per book. Other applications on the mobile phone will have no interest in the files that the book reader
uses to store books, so those files are tightly bound to the application, and there is no need to make any effort to share the book data. In fact, the Android OS enforces this tight binding so that applications can't read or write data across packages at all.

However, some applications want to share their data; that is, they want other applications to be able to read and write data within their database. Perhaps the most obvious example is contact data. If each application that required contacts forced the user to maintain a separate database for that specific application, the phone would be all but useless.

Android enables applications to share data using the content provider API. This API enables each client application to query the OS for data it's interested in, using a uniform resource identifier (URI) mechanism, similar to the way a browser requests information from the Internet.

The client does not know which application will provide the data; it simply presents the OS with a URI and leaves it to the OS to start the appropriate application to provide the result.

The content provider API enables full CRUD access to the content. This means the application can:

  • Create new records

  • Retrieve one, all, or a limited set of records

  • Update records

  • Delete records if permitted

This section shows how to use the content provider API by examining the inner workings of the NotePad application provided with the Android SDK. Assuming the SDK was installed in the /sdk directory, all file references within the NotePad project are relative to /sdk/samples/NotePad; thus, when the AndroidManifest.xml file is referenced in this section, the /sdk/samples/NotePad/AndroidManifest.xml file is assumed. By studying NotePad's implementation, you'll be able to create and manage content providers of your own.

NOTE

Throughout this chapter we make the assumption that the backend of a content provider is a SQLite database. This will almost always be the case, and the API uses standard database operations, such as create, read, update, and delete. However, it is possible to use the API to store and retrieve data using any backend that will support the required operations. For instance, a flat file that just does inserts and queries that return some subset of the file is possible. However, in most cases an SQLite database will be on the backend of a content provider, so we use those terms and concepts in this chapter.

8.2.1. Introducing NotePad

The Android NotePad application is a very simple notebook. It allows the user to type textual notes on lined note paper and store them under a textual title of any length. A user can create notes, view a list of notes, and update and delete notes. As an application, NotePad is usable, but just barely; its main purpose is to show programmers how to build and use content providers.

8.2.1.1. Activities

The NotePad application has three distinct Activities: NoteList, NoteEditor, and TitleEditor. Instead of communicating directly to the NotePad database, each of these Activities use the content provider API, so the NotePad application is both a content provider client and a server. This makes it perfect for exploring content providers.

The purpose of each activity is reasonably obvious from its name. The NoteList activity presents the user with a list of notes, and allows her to add a new note or edit the title or body of an existing note.

The NoteEditor allows a user to create a new note or modify the body of an existing note. Finally, the TitleEditor is a dialog box that allows a user to modify the title of an existing note.

8.2.1.2. Database

The NotePad database is created with the following SQL statement:

CREATE TABLE notes (
    _id INTEGER PRIMARY KEY,
    title TEXT,
    note TEXT,
    created INTEGER,
    modified INTEGER
);

The _id column is not required, but recommended by the Android SDK documentation. The documentation suggests that the column should be defined with the SQL attributes INTEGER PRIMARY KEY AUTOINCREMENT. Unless you have an application-specific identifier that you can guarantee to be unique, you might as well make use of the AUTOINCREMENT feature to assign arbitrary integers robustly.

The title and note columns store the note title and note body data, respectively. The main raison d'être for the NotePad application is to manipulate the contents of these columns.

Finally, the created and modified columns keep track of when the note was created and when it was last modified. In the NotePad application itself, these columns are never seen by the user. However, other applications can read them using the content provider API.

8.2.1.3. Structure of the source code

This section briefly examines each relevant file within the NotePad application:


AndroidManifest.xml

Chapter 3 described the purpose of the AndroidManifest.xml file that is part of every Android application. It describes important attributes of the application, such as the Activities and Intents that the application implements. The AndroidManifest.xml file for the NotePad application reveals the three activities—NotesList, NoteEditor, and TitleEditor—along with the various Intents that these activities consume. Finally, the <provider> element shows that the application is a content provider. We'll discuss the <provider> element in detail later in this section.


res/drawable/app_notes.png

This file is the icon for the application. The <application> element within the AndroidManifest.xml file sets the icon using the android:icon attribute.


res/layout/*.xml

These three layout files use XML to describe how each activity screen is laid out. Chapter 2 covers these concepts.


res/values/strings.xml

All of the user-visible strings in the NotePad application appear in this file. Over time, as the application gains acceptance in the user community, users from non-English-speaking countries will want the application adapted to their languages. This job is much easier if all user-facing strings start out in strings.xml.


src/com/example/android/notepad/NoteEditor.java

The NoteEditor class extends the Activity class and allows the user to edit a note in the notes database. This class never manipulates the notes database directly, but instead uses the NotePadProvider content provider.


src/com/example/android/notepad/NotePad.java

The NotePad class contains the AUTHORITY attribute (discussed later) and the Notes class, which defines the names of the content provider columns. Because the database columns are named the same as the content provider columns, the Note class also is also used to define the names of the database columns. Neither the NotePad class nor the Notes class contain any executable code. The relevant portion of the NotePad.java file follows:

public final class NotePad {
    public static final String AUTHORITY = "com.google.provider.NotePad";
    private NotePad() {}// This class cannot be instantiated
    /** Notes table */
    public static final class Notes implements BaseColumns {
        // This class cannot be instantiated
        private Notes() {} // This class cannot be instantiated
        public static final Uri CONTENT_URI =
                Uri.parse("content://" + AUTHORITY + "/notes");
        public static final String CONTENT_TYPE =
                "vnd.android.cursor.dir/vnd.google.note";
        public static final String CONTENT_ITEM_TYPE=
                "vnd.android.cursor.item/vnd.google.note";
        public static final String TITLE = "title";
        public static final String NOTE = "note";
        public static final String CREATED_DATE = "created";
        public static final String MODIFIED_DATE = "modified";
    }
}

src/com/example/android/notepad/NotePadProvider.java

The NotePadProvider class is the content provider for the notes database. It intercepts URIs for each of the CRUD actions and returns data appropriate to the action requested. This file is examined in detail later in this chapter.


src/com/example/android/notepad/NotesList.java

The NotesList class is an Activity that allows the user to view a list of notes. The user can add a new note or edit the title or body of an existing note


src/com/example/android/notepad/TitleEditor.java

The TitleEditor class is an Activity that implements a dialog box that allows a user to modify the title of an existing note. Since this is a very simple class, it is quite helpful to examine it closely, to understand how to query and modify data in a content provider.

8.2.2. Content Providers

Now that we've examined the general structure of the NotePad application, it's time to look at how the application both implements and consumes the NotePadProvider content provider.

8.2.2.1. 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.

8.2.2.1.1. 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.

8.2.2.1.2. 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.onUp⁠date method. See Section 8.2.2.1.4 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.

8.2.2.1.3. 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");
8.2.2.1.4. 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:

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.


8.2.2.1.5. 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 Section 8.2.2.1.2 is available to handle these complexities.

8.2.2.1.6. Supporting binary data

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

8.2.2.1.7. Declare column specification strings

The NotePadProvider columns are defined in the NotePad.Notes class, as mentioned in Section 8.2.2.1.2. 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.

8.2.2.1.8. 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.

8.2.2.1.9. 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);
}
8.2.2.1.10. 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 SQLiteQueryBuilder 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;
}
8.2.2.1.11. 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;
}
8.2.2.1.12. 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;
}
8.2.2.1.13. 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."

8.2.2.1.14. 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.goo⁠gle.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 Section 8.2.1.3. The return value starts with vnd.android.cur⁠sor.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);
  }
}

8.2.3. Consuming a Content Provider

The NotePad application both implements and consumes the NotePadProvider content provider. The previous sections described how the NotePadProvider allows any application on the Android device to access the notes database. This section explains how the various Activities use the NotePadProvider to manipulate the database. Since these activities are part of the same application as the NotePadProvider, they could simply manipulate the database directly, but instead they use the ContentProvider. This does not impose any performance penalty, so not only does it work well as an example for our purposes, but it is also good programming practice for all applications implementing a content provider.

The following sections follow the CRUD functions in order. First, data is created using the SQL INSERT statement. That data is then typically read using an SQL SELECT query. Sometimes the data must be updated using the SQL UPDATE statement or deleted using the SQL DELETE statement.

8.2.3.1. Create data (insert)

The following code is from the NoteEditor class in the NotePad application. Code that was not relevant to the discussion was removed in the listing:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final Intent intent = getIntent();

    // Do some setup based on the action being performed.
    final String action = intent.getAction();
    if (Intent.ACTION_EDIT.equals(action)) {
        ...
    } else if (Intent.ACTION_INSERT.equals(action)) {
        // Requested to insert: set that state, and create a new entry
        // in the container.
        mUri = getContentResolver().insert(intent.getData(), null);

        if (mUri == null) {
            // Creating the new note failed
            finish();
            return;
        }
        // Do something with the new note here.
        ...

    }
    ...
}

The NotePad application starts out in the NotesList Activity. NotesList has an "Add Note" menu entry, shown in Figure 8-1.

Figure 8-1. NotesList Activity


When the user presses the Add Note button, the NoteEditor Activity is started with the ACTION_INSERT Intent. NoteEditor's onCreate method examines the Intent to determine why it was started. When the Intent is ACTION_INSERT, a new note is created by calling the insert method of the content resolver:

mUri = getContentResolver().insert(intent.getData(), null);        

In brief, this line's job is to create a new blank note and return its URI to the mUri variable. The value of the mUri variable is the URI of the note being edited.

So how does this sequence of calls work? First, note that NotesList's parent class is ListActivity. All Activity classes are descended from ContextWrapper. So, the first thing the line does is call ContextWrapper.getContentResolver to return a ContentResolver instance. The insert method of that ContentResolver is then immediately called with two parameters:


URI of the content provider in which to insert the row

Our argument, intent.getData, resolves to the URI of the Intent that got us here in the first place, content://com.google.provider.NotePad/notes.


Data to insert

Here, by passing null, we're inserting a record with no data. The data is added later with a call to the update method when the user types something in.

ContentResolver's job is to manipulate objects that URIs point to. Almost all of its methods are verbs that take a URI as their first argument. ContentResolver's methods include all of the CRUD methods, stream methods for file I/O, and others.

8.2.3.2. Read/query data

To read data, use the managedQuery method. This is an Activity method that calls query internally. It manages the query for the developer, closing the Cursor and requerying it when necessary. The parameters passed to managedQuery are:


uri

The URI to query. This will map to a specific content provider, and in NotePad's case, to the NotePad content provider.


projection

A String array with one element for each column you want returned in the query. Columns are numbered and correspond to the order of the columns in the underlying database.


selection

Indicates which rows to retrieve through an SQL WHERE clause; it is passed as a single String variable. Can be NULL if you want all rows.


selectionArgs

A String array containing one argument for each parameter or placeholder (a question mark in the SQL SELECT statement). Pass NULL if there are no arguments.


sortOrder

A String variable containing a full ORDER BY argument, if sorting is desired. Can be NULL.

The NotePad application queries the NotePadProvider to fill in the list of notes to display to the user:

public class NotesList extends ListActivity {

    ...

    private static final String[] PROJECTION = new String[] {
            Notes._ID, // 0
            Notes.TITLE, // 1
    };

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);

        // If no data was given in the Intent (because we were started
        // as a MAIN activity), then use our default content provider.
        Intent intent = getIntent();
        if (intent.getData() == null) {
            intent.setData(Notes.CONTENT_URI);
        }

        // Inform the list we provide context menus for items
        getListView().setOnCreateContextMenuListener(this);

        // Perform a managed query. The Activity will handle closing
        // and requerying the cursor when needed.
        Cursor cursor = managedQuery(getIntent().getData(),
                PROJECTION, null, null, Notes.DEFAULT_SORT_ORDER);

        // Used to map notes entries from the database to views
        SimpleCursorAdapter adapter = new SimpleCursorAdapter(
            this,
            R.layout.noteslist_item,
            cursor,
            new String[] { Notes.TITLE },
            new int[] { android.R.id.text1 });
        setListAdapter(adapter);
    }

Here are some of the highlights of the code:

8.2.3.3. Update data

To understand how to update data, we'll take a look at the TitleEditor class. Because it's small, looking at it in its entirety is instructive. Relatively few lines are needed to manipulate the content provider, and most of the function connects the user's clicks to changes in the content provider. The user interaction uses basic manipulations of graphic elements, which were briefly introduced in Chapter 4 and will be fully discussed in Chapter 10 and subsequent chapters. The rest of this section prints the TitleEdi⁠tor class in blocks, following each block with explanations.

public class TitleEditor extends Activity implements View.OnClickListener {

    /** An array of the columns we are interested in. */
    private static final String[] PROJECTION = new String[] {
            NotePad.Notes._ID, // 0
            NotePad.Notes.TITLE, // 1
    };

    /** Index of the title column */
    private static final int COLUMN_INDEX_TITLE = 1;

    /** Cursor providing access to the note whose title we are editing. */
    private Cursor mCursor;

    /** The EditText field from our UI. Used to extract the text when done. */
    private EditText mText;

    /** The content URI to the note that's being edited. */
    private Uri mUri;

This first section of the TitleEditor Activity class sets up all of its private data. The following private variables are declared:


PROJECTION

Used by the managedQuery function to describe the columns to return in the query, as shown in the previous section.


COLUMN_INDEX_TITLE

Defines the number of the column, in the order returned by the query, from which the title must be pulled. The numbers start at 0, so the value of 1 shown is the index of the TITLE within the PROJECTION string.


mUri

Holds the URI of the note whose title we're going to edit. An example URI might be content://com.google.provider.NotePad/notes/2.


mCursor

The cursor that holds the results of the query.


mText

The EditText field on the form.

Next, the Activity's onCreate method sets up the Activity:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.title_editor);

    // Get the uri of the note whose title we want to edit
    mUri = getIntent().getData();

    // Get a cursor to access the note
    mCursor = managedQuery(mUri, PROJECTION, null, null, null);

    // Set up click handlers for the text field and button
    mText = (EditText) this.findViewById(R.id.title);
    mText.setOnClickListener(this);

    Button b = (Button) findViewById(R.id.ok);
    b.setOnClickListener(this);
}

Here are some of the highlights of the code:

When onCreate finishes, the onResume method is called. This method pulls the current value of the note title from the cursor and assigns it to the value of the text box:

@Override
protected void onResume() {
    super.onResume();

    // Initialize the text with the title column from the cursor
    if (mCursor != null) {
        mCursor.moveToFirst();
        mText.setText(mCursor.getString(COLUMN_INDEX_TITLE));
    }
}

The onPause method is where the application writes the data back to the database. In other words, NotePad follows the typical Android practice of saving up writes until the application is suspended. We'll see soon where this method is called:

@Override
protected void onPause() {
    super.onPause();

    if (mCursor != null) {
        // Write the title back to the note
        ContentValues values = new ContentValues();
        values.put(Notes.TITLE, mText.getText().toString());
        getContentResolver().update(mUri, values, null, null);
    }
}

Here are some of the highlights of the code:

The last method in TitleEditor is the common callback for handling user clicks, named onClick:

public void onClick(View v) {
    // When the user clicks, just finish this activity.
    // onPause will be called, and we save our data there.
    finish();
}

The comment describes what is going on pretty well. Once the user clicks either the OK button or the text box within the dialog box, the Activity calls the finish method. That method calls onPause, which writes the contents of the dialog box back to the database, as we showed earlier.

8.2.3.4. Delete data

A user who pulls up a list of notes from the NotesList class can choose the Delete option on the context menu to run the following method:

@Override
public boolean onContextItemSelected(MenuItem item) {
  AdapterView.AdapterContextMenuInfo info;
  info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();

  switch (item.getItemId()) {
    case MENU_ITEM_DELETE: {
      // Delete the note that the context menu is for
      Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
      getContentResolver().delete(noteUri, null, null);
      return true;
    }
  }
  return false;
}

Here are some of the highlights of the code:

          
      Previous section   Next section