To accomplish many of the activities offered by modern mobile phones, such as tracking contacts, events, and tasks, the operating system and applications must be adept at storing and keeping track of large quantities of data. Most of this data is structured like a spreadsheet, in the form of rows and columns. Each Android application is like an island unto itself, in that each application is only allowed to read and write data that it has created, but sharing data across application boundaries is necessary. Android supports the content provider feature mentioned in Chapter 1 so that applications can share data.
In this chapter we examine two distinct data access APIs that the Android framework offers:
Android’s Java interface to its relational database, SQLite. It supports an SQL implementation rich enough for anything you’re likely to need in a mobile application, including a cursor facility.
An interface used between applications. The server application that hosts the data manages it through basic create, read, update, and delete (CRUD) operations. The client application uses a similar API, but the Android framework transmits the client’s requests to the server. We’ll show both the server API and the client API in this chapter.
Data is best stored in a relational database format if it can include many instances of the same type of thing. Take a contact list, for instance. There are many contacts, all of whom potentially have the same types of information (address, phone number, etc.). Each “row” of data stores information about a different person, while each “column” stores a specific attribute of each person: names in one column, addresses in another column, and home phone numbers in a third.
Android uses the SQLite
database engine, a self-contained, transactional database engine that requires no separate server
process. It is used by many applications and environments beyond
Android, and is being actively developed by a large community.
The process that initiates a database operation, such as a
SELECT
or UPDATE
, does the actual work of reading or writing the disk file that
contains the database in order to fulfill the request. With SQLite,
the database is a simple disk file. All of the data structures making
up a relational database—tables, views, indexes, etc.—are within this
file.
SQLite is not a Google project, although Google has contributed to it. SQLite has an international team of software developers who are dedicated to enhancing the software’s capabilities and reliability. Some of those developers work full time on the project.
Reliability is a key feature of SQLite. More than half of the code in the project is devoted to testing the library. The library is designed to handle many kinds of system failures, such as low memory, disk errors, and power failures. In no case should the database be left in an unrecoverable state: this would be a showstopper on a mobile phone, where critical data is often stored in a database. If that database were susceptible to easy corruption, the mobile phone could become an expensive paperweight if the battery were to fail at an inopportune time.
This is not a book on SQL, so we will not go into much detail about the database commands themselves. Ample documentation about SQL in general and SQLite in particular can be found on the Web. But the SQL we use in our examples should be a good starting point for your own applications.
We’ll use the MicroJobsDatabase.java file from our
MicroJobs example application to discuss how to create and use a
SQLite
database using Android. This
is the subject of the next section.
In our example, the MicroJobsDatabase.java file completely encapsulates all of the SQL logic necessary to work with the database. All of the other Java classes in the MicroJobs application work with standard Java classes or Cursors and are unaware of how the data is actually stored. This is good programming practice and should be emulated in all of your Android applications that use databases.
Before we delve too deeply into the guts of creating a
database and selecting data from it, it’s important to understand
the general layout of the MicroJobsDatabase
class.
MicroJobsDatabase
inherits
from the abstract SQLiteOpenHelper
class, and therefore must override the onCreate
and
onUpgrade
methods. The onCreate
method is automatically called
when the application starts for the first time; its job is to create
the database. As newer versions of the application are shipped, the
database on the phone tends to be updated, a task that falls to the
onUpgrade
method. When you ship a
new version of a database, you must also increment the version
number, as we’ll explain.
The general elements in MicroJobsDatabase
code are:
The MicroJobsDatabase
class defines two important constants:
DATABASE_NAME
This holds the filename of the database, "MicroJobs"
in this
case.
Here is the full path to the MicroJobs file:
/data/data/com.microjobsinc.mjandroid/databases/MicroJobs.
You can use the adb
pull
command line on your desktop (see the
discussion of adb
in The Tools) to pull the
database from the emulator or developer device and
then debug it using the SQLite3
executable on the desktop.
DATABASE_VERSION
This defines the database version understood by
the software that defines the constant. If the
version of the database on the machine is less than
DATABASE_VERSION
, the
application should run onUpgrade
to upgrade the
database to the current level.
The constructor for the database in this program, MicroJobsDatabase
, uses the super
function to call its parent’s constructor. The parent
does most of the work of creating the database object. One
thing our MicroJobsDatabase
constructor has to do is store the Context object. This step
is not required in applications whose database code is
encapsulated within an enclosing content provider class,
because the ContentProvider
class has a getContext
call
that will provide the Context
object when necessary. Since
MicroJobs is a standalone database class, it has to keep the
Context
object around in
its own private variable. In the case of MicroJobs, the
Context
object is really
the Activity
object that opens the database. An Activity is a Context. The
Context
object is the
interface to application-global resources and classes as well
as application-level operations, such as broadcasting Intents
and launching activities.
onCreate
When an Android application attempts to read or write
data to a database that does not exist, the framework executes
the onCreate
method. The
onCreate
method in the
MicroJobsDatabase
class
shows one way to create the database. Because so much SQL code
is required to create the database and populate it with sample
data, we’ve chosen to segregate all of the SQL code invoked by
onCreate
into the strings.xml resource file; this
makes the Java code much more readable but forces the
developer to look in two separate files to see what’s really
going on. When we look at the custom Cursor classes later in
this chapter, we’ll see that SQL can be embedded into the
application source code as well. It’s really a matter of
style.
To actually create the database, the first line of the
onCreate
method loads the
SQL string referenced by
the MicroJobsDatabase_onCreate
resource
identifier into a String
array named sql
. Note the following code
snippets from MicroJobsDatabase.java:
String[] sql = mContext.getString(R.string.MicroJobsDatabase_onCreate).split(" ");
and from strings.xml:
<string name="MicroJobsDatabase_onCreate">" CREATE TABLE jobs (_id INTEGER PRIMARY KEY AUTOINCREMENT, employer_id INTEGER, title TEXT, description TEXT, start_time INTEGER, end_time INTEGER, status INTEGER); CREATE TABLE employers( _id INTEGER, employer_name TEXT, ... CREATE TABLE workers( _id INTEGER PRIMARY KEY AUTOINCREMENT, ... CREATE TABLE status( _id INTEGER PRIMARY KEY AUTOINCREMENT, ... INSERT INTO status (_id , status) VALUES (NULL, 'Filled'), INSERT INTO status (_id , status) VALUES (NULL, 'Applied For'), INSERT INTO status (_id , status) VALUES (NULL, 'Open'), ... "</string>
The single getString
line of Java code loads the SQL required to create the
database, along with a reasonable amount of test data.
One crucial piece of information mentioned only
briefly in the Android documentation is that you must either
escape all single quotes and double quotes with a
backslash ("
or '
) within a resources string or
enclose the entire string in either single or double quotes.
If single and double quotes are mixed in a resource string, they must be escaped.
In the case of the MicroJobsDatabase_onCreate
string
just shown, notice that the entire thing is surrounded with
double quotes.
The rest of the onCreate
method runs each line of
SQL
. The entire process
runs under a transaction so that it will either execute
completely or be rolled back and have no effect at all on the
database.
onUpdate
In the MicroJobs application, the onUpdate
method is very similar in structure to the onCreate
method. However, the
contents of the strings.xml resource file are quite
different:
<string name="MicroJobsDatabase_onUpgrade">" DROP TABLE IF EXISTS jobs DROP TABLE IF EXISTS employers DROP TABLE IF EXISTS workers DROP TABLE IF EXISTS status "</string>
The opening <string>
tag is followed by a
double quotation mark to start a string, and a closing
quotation mark ends the strings before the </string>
tag. Within the
string are four rather drastic SQL commands. To support the
demonstration code in this book, we cheat a little. The
“upgrade” code removes the old database and re-creates it with
whatever is in the current version of the code. Although this
is nice for a book, it won’t work very well in real life. Your
customers won’t be very happy if they have to re-key their
information each time they upgrade software versions! A real
application would have several upgrade scripts, one for each
version that might be out in the wild. We would execute each
upgrade script, one at a time, until the phone’s database is
completely up-to-date.
The structural parts of MicroJobsDatabase.java
follow. The custom
Cursors and the public functions that return them are discussed
next.
MicroJobsDatabase.java (structure): package com.microjobsinc.mjandroid; import ... /** * Provides access to the MicroJobs database. Since this is not a Content Provider, * no other applications will have access to the database. */ public class MicroJobsDatabase extends SQLiteOpenHelper { /** The name of the database file on the file system */ private static final String DATABASE_NAME = "MicroJobs"; /** The version of the database that this class understands. */ private static final int DATABASE_VERSION = 1; /** Keep track of context so that we can load SQL from string resources */ private final Context mContext; /** Constructor */ public MicroJobsDatabase(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); this.mContext = context; } /** Called when it is time to create the database */ @Override public void onCreate(SQLiteDatabase db) { String[] sql = mContext.getString(R.string.MicroJobsDatabase_onCreate).split(" "); db.beginTransaction(); try { // Create tables and test data execMultipleSQL(db, sql); db.setTransactionSuccessful(); } catch (SQLException e) { Log.e("Error creating tables and debug data", e.toString()); throw e; } finally { db.endTransaction(); } } /** Called when the database must be upgraded */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(MicroJobs.LOG_TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); String[] sql = mContext.getString(R.string.MicroJobsDatabase_onUpgrade).split(" "); db.beginTransaction(); try { execMultipleSQL(db, sql); db.setTransactionSuccessful(); } catch (SQLException e) { Log.e("Error upgrading tables and debug data", e.toString()); throw e; } finally { db.endTransaction(); } // This is cheating. In the real world, you'll need to add columns, not rebuild from scratch. onCreate(db); } /** * Execute all of the SQL statements in the String[] array * @param db The database on which to execute the statements * @param sql An array of SQL statements to execute */ private void execMultipleSQL(SQLiteDatabase db, String[] sql){ for( String s : sql ) if (s.trim().length()>0) db.execSQL(s); } }
Here are some of the highlights of the code:
Constructs the MicroJobsDatabase
object. We pass the
parent class the database name and version, and it keeps track
of when to simply open the database and when to upgrade the
version. The database itself is not opened here—that happens in
response to a getReadableDatabase
or getWritableDatabase
call. We also keep
a private reference to the Context
object in the
constructor.
Retrieves strings containing SQL code, which we have chosen to store in a resource file for easier readability and maintenance.
Begins the transaction within which all the SQL statements will execute to create the database.
Ends the transaction, creating the database.
Function to call in order to upgrade the database.
Function that executes each SQL statement retrieved by item 2.
There are many ways to read data from an SQL database, but they all come down to a basic sequence of operations:
Create an SQL statement that describes the data that you need to retrieve.
Execute that statement against the database.
Map the resulting SQL data into data structures that the language you’re working in can understand.
This process can be very complex in the case of object-relational mapping software, or relatively simple when writing the queries directly into your application. The difference is fragility. Complex ORM tools shield your code from the complexities of database programming and object mapping by moving that complexity elsewhere. The result is that your code is more robust in the face of database changes, but at the cost of complex ORM setup and maintenance.
The simple approach of writing queries directly into your application works well only for very small projects that will not change much over time. Applications with database code in them are very fragile because as the database changes, any code that references those changes must be examined and potentially changed.
A common middle-ground approach is to sequester all of the database logic into a set of objects whose sole purpose is to translate application requests into database requests and deliver the results back to the application. This is the approach we have taken with the MicroJobs application; all of the database code is contained in a single class in the file MicroJobsDatabase.java.
Android gives us the ability to customize Cursors, and we use that ability to further reduce code
dependencies by hiding all of the information about each specific
database operation inside a custom cursor. Each custom cursor is a
class within the MicroJobsDatabase
class; the one
that we’ll look at in this chapter is the JobsCursor
.
The interface to the caller in the getJobs
method of MicroJobsDatabase
appears first in the
code that follows. The method’s job is to return a JobsCursor
filled with jobs from the
database. The user can choose (through the single parameter passed
to getJobs
) to sort jobs by
either the title
column or the
employer_name
column:
public class MicroJobsDatabase extends SQLiteOpenHelper { ... /** Return a sorted JobsCursor * @param sortBy the sort criteria */ public JobsCursor getJobs(JobsCursor.SortBy sortBy) { String sql = JobsCursor.QUERY + sortBy.toString(); SQLiteDatabase d = getReadableDatabase(); JobsCursor c = (JobsCursor) d.rawQueryWithFactory( new JobsCursor.Factory(), sql, null, null); c.moveToFirst(); return c; } ... public static class JobsCursor extends SQLiteCursor{ public static enum SortBy{ title, employer_name } private static final String QUERY = "SELECT jobs._id, title, employer_name, latitude, longitude, status "+ "FROM jobs, employers "+ "WHERE jobs.employer_id = employers._id "+ "ORDER BY "; private JobsCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { super(db, driver, editTable, query); } private static class Factory implements SQLiteDatabase.CursorFactory{ @Override public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { return new JobsCursor(db, driver, editTable, query); } } public long getColJobsId() {return getLong(getColumnIndexOrThrow("jobs._id"));} public String getColTitle() {return getString(getColumnIndexOrThrow("title"));} public String getColEmployerName() {return getString(getColumnIndexOrThrow("employer_name"));} public long getColLatitude() {return getLong(getColumnIndexOrThrow("latitude"));} public long getColLongitude() {return getLong(getColumnIndexOrThrow("longitude"));} public long getColStatus(){return getLong(getColumnIndexOrThrow("status"));} }
Here are some of the highlights of the code:
Function that fashions a query based on the user’s
requested sort column (the sortBy
parameter) and returns results
as a cursor.
Creates the query string. Most of the string is static
(the QUERY
variable), but
this line tacks on the sort column. Even though QUERY
is private, it is still
available to the enclosing class. This is because the getJobs
method and the the JobsCursor
class are both within the
MicroJobsDatabase
class,
which makes JobsCursor
’s
private data members available to the getJobs
method.
To get the text for the sort column, we just run toString
on the enumerated value
passed by the user. The enumeration is defined at item 8. We
could have defined an associative array, which would give us
more flexibility in naming variables, but this solution is
simpler. Additionally, the names of the columns pop up quite
nicely in Eclipse’s autocompletion.
Retrieves a handle to the database.
Creates the JobsCursor
cursor using the SQLiteDatabase
object’s rawQueryWithFactory
method. This
method lets us pass a factory method that Android will use to
create the exact type of cursor we need. If we had used the
simpler rawQuery
method, we
would get back a generic Cursor
that lacked the special
features of JobsCursor
.
As a convenience to the caller, moves to the first row in
the result. This way, the cursor is returned ready to use. A
common mistake is forgetting the moveToFirst
call and then pulling your
hair out trying to figure out why the Cursor
object is throwing
exceptions.
The cursor is the return value.
Class that creates the cursor returned by getJobs
.
Simple way to provide alternate sort criteria: store the
names of columns in an enum
.
This variable is used in item 2.
Constructor for the customized cursor. The final argument is the query passed by the caller.
Factory
class to create
the cursor, embedded in the JobsCursor
class.
Creates the cursor from the query passed by the caller.
Returns the cursor to the enclosing JobsCursor
class.
Convenience functions that extract particular columns from
the row under the cursor. For instance, getColTitle
returns the value of the
title
column in the row
currently referenced by the cursor. This separates the database
implementation from the calling code and makes that code easier
to read.
A sample use of the database follows. The code gets a cursor,
sorted by title, through a call to getJobs
. It then iterates through the
jobs.
MicroJobsDatabase db = new MicroJobsDatabase(this); JobsCursor cursor = db.getJobs(JobsCursor.SortBy.title); for( int rowNum=0; rowNum<cursor.getCount(); rowNum++){ cursor.moveToPosition(rowNum); doSomethingWith(cursor.getColTitle()); }
Here are some of the highlights of the code:
Creates a MicroJobsDatabase
object. The
argument, this
, represents
the context, as discussed previously.
Creates the JobsCursor
cursor, referring to the SortBy
enumeration discussed
earlier.
Uses generic Cursor methods to iterate through the cursor.
Still within the loop, invokes one of the custom accessor
methods provided by JobsCursor
to “do something” chosen by
the user with the value of each row’s title column.
Android Cursors are great when you want to read data from the
database, but the Cursors API does not provide methods for creating,
updating, or deleting data. The SQLiteDatabase
class provides two basic interfaces that you can use for
both reading and writing:
We recommend using the first method when your operations fit its capabilities. We’ll show you both ways using the MJAndroid operations.
The SQL INSERT
statement
is used whenever you want to insert data into an SQL
database. The INSERT
statement maps to the “create”
operation of the CRUD methodology.
In the MJAndroid application, the user can add jobs to the list by clicking on the Add Job menu item when looking at the Jobs list. The user can then fill out a form to input the employer, job title, and description. After the user clicks on the Add Job button on the form, the following line of code is executed:
db.addJob(employer.id, txtTitle.getText().toString(), txtDescription.getText().toString());
This code calls the addJob
function, passing in the employer
ID, the job title, and the job description. The addJob
function does the actual work of
writing the job out to the database.
Example 8-1 shows you how to
use the insert
method.
/** * Add a new job to the database. The job will have a status of open. * @param employer_id The employer offering the job * @param title The job title * @param description The job description */ public void addJob(long employer_id, String title, String description){ ContentValues map = new ContentValues(); map.put("employer_id", employer_id); map.put("title", title); map.put("description", description); try{ getWritableDatabase().insert("jobs", null, map); } catch (SQLException e) { Log.e("Error writing new job", e.toString()); } }
Here are some of the highlights of the code in Example 8-1:
The ContentValues
object is a map of column names to column values. Internally,
it’s implemented as a HashMap<String,Object>
.
However, unlike a simple HashMap
, ContentValues
is strongly typed. You
can specify the data type of each value stored in a ContentValues
container. When trying
to pull values back out, ContentValues
will automatically
convert values to the requested type if possible.
The second parameter to the insert
method is nullColumnHack
. It’s used only when
the third parameter, the map, is null and therefore the row
would otherwise be completely empty.
Example 8-2 shows you how to
use the execSQL
method.
/** * Add a new job to the database. The job will have a status of open. * @param employer_id The employer offering the job * @param title The job title * @param description The job description */ public void addJob(long employer_id, String title, String description){ String sql = "INSERT INTO jobs (_id, employer_id, title, description, start_time, end_time, status) " + "VALUES ( NULL, ?, ?, ?, 0, 0, 3)"; Object[] bindArgs = new Object[]{employer_id, title, description}; try{ getWritableDatabase().execSQL(sql, bindArgs); } catch (SQLException e) { Log.e("Error writing new job", e.toString()); } }
Here are some of the highlights of the code in Example 8-2:
First, we build a SQL string template named sql
that contains bindable
parameters that will be filled in with user data. The bindable
parameters are marked by a question mark in the string. Next,
we build an object array named bindArgs
that contains one object
per element in our SQL template. There are three question
marks in the template, and therefore there must be three
elements in the object array.
Executes the SQL command by passing the SQL template
string and the bind arguments to execSQL
. Using a SQL template and
bind arguments is much preferred over building up the SQL
statement, complete with parameters, into a String
or StringBuilder
. By using a
template with parameters, you protect your application from
SQL injection attacks. These attacks occur when a malicious
user enters information into a form that is deliberately meant
to modify the database in a way that was not intended by the
developer. This is normally done by ending the current SQL
command prematurely, using SQL syntax characters, and then
adding new SQL commands
directly in the form field. The template-plus-parameters
approach also protects you from more run-of-the-mill errors,
such as invalid characters in the parameters.
The MicroJobs application enables the user to edit a job by
clicking on the job in the Jobs list and choosing the Edit Job
menu item. The user can then modify the strings for employer, job
title, and description in the editJob
form. After the user clicks on
the Update button on the form, the following line of code is
executed:
db.editJob((long)job_id, employer.id, txtTitle.getText().toString(), txtDescription.getText().toString());
This code calls the editJob
method, passing the job ID and
the three items the user can change: employer ID, job title, and
job description. The editJob
method does the actual work of modifying the job in the
database.
Example 8-3 shows you how to
use the update
method.
/** * Update a job in the database. * @param job_id The job id of the existing job * @param employer_id The employer offering the job * @param title The job title * @param description The job description */ public void editJob(long job_id, long employer_id, String title, String description) { ContentValues map = new ContentValues(); map.put("employer_id", employer_id); map.put("title", title); map.put("description", description); String[] whereArgs = new String[]{Long.toString(job_id)}; try{ getWritableDatabase().update("jobs", map, "_id=?", whereArgs); } catch (SQLException e) { Log.e("Error writing new job", e.toString()); } }
Here are some of the highlights of the code in Example 8-3:
The first parameter to update
is the name of the table to
manipulate. The second is the map of column names to new
values. The third is a small snippet of SQL; in this case,
it’s a SQL template with one parameter. The parameter is
marked with a question mark, and is filled out with the
contents of the fourth argument.
Example 8-4 shows you how
to use the execSQL
method.
/** * Update a job in the database. * @param job_id The job id of the existing job * @param employer_id The employer offering the job * @param title The job title * @param description The job description */ public void editJob(long job_id, long employer_id, String title, String description) { String sql = "UPDATE jobs " + "SET employer_id = ?, "+ " title = ?, "+ " description = ? "+ "WHERE _id = ? "; Object[] bindArgs = new Object[]{employer_id, title, description, job_id}; try{ getWritableDatabase().execSQL(sql, bindArgs); } catch (SQLException e) { Log.e("Error writing new job", e.toString()); } }
For the application in Example 8-4, we show the simplest
possible function. This makes it easy to understand in a book, but
is not enough for a real application. In a real application, you
would want to check input strings for invalid characters, verify
that the job exists before trying to update it, verify that the
employer_id
value is valid
before using it, do a better job of catching errors, etc. You
would also probably authenticate the user for any application that
is shared by multiple people.
The MicroJobs application enables the user to delete a job as well as create and change it. From the main application interface, the user clicks on the List Jobs button to get a list of jobs, and then clicks on a particular job to see the job detail. At this level, the user can click on the “Delete this job” menu item to delete the job. The application asks the user if he really wants to delete the job. When the user hits the “Delete” button in response, the following line of code in the MicroJobsDetail.java file is executed:
db.deleteJob(job_id);
This code calls the deleteJob
method of the MicroJobsDatabase
class, passing it the
job ID to delete. The code is similar to the functions we’ve
already seen and lacks the same real-world features.
Example 8-5 shows you how to
use the delete
method.
/** * Delete a job from the database. * @param job_id The job id of the job to delete */ public void deleteJob(long job_id) { String[] whereArgs = new String[]{Long.toString(job_id)}; try{ getWritableDatabase().delete("jobs", "_id=?", whereArgs); } catch (SQLException e) { Log.e("Error deleteing job", e.toString()); } }
Example 8-6 shows you how
to use the execSQL
method.
/** * Delete a job from the database. * @param job_id The job id of the job to delete */ public void deleteJob(long job_id) { String sql = String.format( "DELETE FROM jobs " + "WHERE _id = '%d' ", job_id); try{ getWritableDatabase().execSQL(sql); } catch (SQLException e) { Log.e("Error deleteing job", e.toString()); } }
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.
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.
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.
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.
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.
This section briefly examines each relevant file within the NotePad application:
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.
This file is the icon for the application. The
<application>
element within the AndroidManifest.xml file sets the
icon using the android:icon
attribute.
These three layout files use XML to describe how each activity screen is laid out. Chapter 2 covers these concepts.
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.
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.
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"; } }
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.
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
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.
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.
The 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.
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.
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 for
more information.
NOTES_TABLE_NAME
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.
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.
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 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");
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.
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 is available to handle these complexities.
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.
The NotePadProvider
columns are defined in the NotePad.Notes
class, as
mentioned in NotePadProvider class and instance variables. 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.
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.
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); }
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; }
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 whereArg
s, 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; }
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; }
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.”
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.
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); } }
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.
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:
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.
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.
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:
Creates the projection, the first parameter to managedQuery
. In this case, the
array contains the note ID and title.
Sets the Activity’s default key handling mode to
DEFAULT_KEYS_SHORTCUTS
.
This lets the user execute shortcut commands from the options
menu without having to press the menu key first.
Gets the client’s request, passed in the Intent. This should contain the content provider URI, but if it doesn’t, the next line sets it to the NotePad URI.
The managedQuery
call, which returns a cursor.
To use the data in the Cursor as the input for a ListActivity, an Adapter is required. In this case, a SimpleCursorAdapter has all the functionality that is necessary.
After you have created the Adapter, issue the
ListActivity’s setListAdapter
method to display the
data from the Cursor on the screen.
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 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:
Finds the ContentView
in the res/layout/title_editor.xml layout
file, using the setContentView
method.
Runs the managedQuery
method to load results into a Cursor.
Sets click handlers for both the button and the text
box. This will direct any clicks on the button or the text box
to the onClick
method,
which we’ll see shortly.
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.
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:
When the menu for the job was created, the job ID was
stuffed into the extra information variable for the menu. That
extra information section is retrieved from the MenuItem
on this line and used in
the next part of the highlighted code.
Builds a URI by extracting the URI from the user’s Intent, as usual, and appending the number of the item to delete, taken from the menu.
Creates a ContentResolver
and pass the URI to
its delete
method.
98.82.120.188