Mobile Phone Handheld Hardware Hardware Rick Rogers John Lombardo O'Reilly Media, Inc. O'Reilly Media Android Application Development, 1st Edition8.2. Content ProvidersMuch 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 NotePadThe 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. ActivitiesThe 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. DatabaseThe 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 codeThis 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 ProvidersNow 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 providerThe Android SDK contains a document that describes nine steps to creating a
content provider. In summary, they are: Extend the ContentProvider class. Define the CONTENT_URI
for your content provider. Create the data storage for your content. Create the column names for communication with
clients. Define the process by which binary data is returned to the
client. Declare public static Strings that clients use to specify
columns. Implement the CRUD methods of a Cursor to return to the
client. Update the AndroidManifest.xml file to declare
your <provider>. 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 ContentProviderWithin 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 variablesAs 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
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_URIWhen 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 storageA 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. 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 namesContent 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 dataWe 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 stringsThe 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 CursorA 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 dataNotePadProvider'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 dataNotePadProvider'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.xmlThe 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 typesYour 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 Section 8.2.1.3. 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);
}
}8.2.3. Consuming a Content ProviderThe 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. 
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 dataTo 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 dataTo 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 TitleEditor 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 dataA 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:
 |