Chapter 8. Persistent Data Storage: SQLite Databases and Content Providers

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:

SQLiteDatabase

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.

ContentProvider

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.

Databases

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.

Basic Structure of the MicroJobsDatabase Class

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:

Constants

The MicroJobsDatabase class defines two important constants:

DATABASE_NAME

This holds the filename of the database, "MicroJobs" in this case.

Note

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.

Constructor

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.

Note

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) {1
        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("
");2
        db.beginTransaction();3
        try {
            // Create tables and test data
            execMultipleSQL(db, sql);
            db.setTransactionSuccessful();4
        } 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) {5
        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){6
        for( String s : sql )
            if (s.trim().length()>0)
                db.execSQL(s);
    }
}

Here are some of the highlights of the code:

1

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.

2

Retrieves strings containing SQL code, which we have chosen to store in a resource file for easier readability and maintenance.

3

Begins the transaction within which all the SQL statements will execute to create the database.

4

Ends the transaction, creating the database.

5

Function to call in order to upgrade the database.

6

Function that executes each SQL statement retrieved by item 2.

Reading Data from the Database

There are many ways to read data from an SQL database, but they all come down to a basic sequence of operations:

  1. Create an SQL statement that describes the data that you need to retrieve.

  2. Execute that statement against the database.

  3. 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) {1
        String sql = JobsCursor.QUERY + sortBy.toString();2
        SQLiteDatabase d = getReadableDatabase();3
        JobsCursor c = (JobsCursor) d.rawQueryWithFactory(4
            new JobsCursor.Factory(),
            sql,
            null,
            null);
        c.moveToFirst();5
        return c;6
    }
...
    public static class JobsCursor extends SQLiteCursor{7
        public static enum SortBy{8
            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) {9
            super(db, driver, editTable, query);
        }
        private static class Factory implements SQLiteDatabase.CursorFactory{10
            @Override
            public Cursor newCursor(SQLiteDatabase db,11
                    SQLiteCursorDriver driver, String editTable,
                    SQLiteQuery query) {
                return new JobsCursor(db, driver, editTable, query);12
            }
        }
        public long getColJobsId()
         {return getLong(getColumnIndexOrThrow("jobs._id"));}13
        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:

1

Function that fashions a query based on the user’s requested sort column (the sortBy parameter) and returns results as a cursor.

2

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.

3

Retrieves a handle to the database.

4

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.

5

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.

6

The cursor is the return value.

7

Class that creates the cursor returned by getJobs.

8

Simple way to provide alternate sort criteria: store the names of columns in an enum. This variable is used in item 2.

9

Constructor for the customized cursor. The final argument is the query passed by the caller.

10

Factory class to create the cursor, embedded in the JobsCursor class.

11

Creates the cursor from the query passed by the caller.

12

Returns the cursor to the enclosing JobsCursor class.

13

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);1
JobsCursor cursor = db.getJobs(JobsCursor.SortBy.title);2

for( int rowNum=0; rowNum<cursor.getCount(); rowNum++){3
    cursor.moveToPosition(rowNum);
    doSomethingWith(cursor.getColTitle());4
}

Here are some of the highlights of the code:

1

Creates a MicroJobsDatabase object. The argument, this, represents the context, as discussed previously.

2

Creates the JobsCursor cursor, referring to the SortBy enumeration discussed earlier.

3

Uses generic Cursor methods to iterate through the cursor.

4

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.

Modifying the Database

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:

  • A set of four methods called simply insert, query, update, and delete

  • A more general execSQL method that takes any SQL statement and runs it against the database

We recommend using the first method when your operations fit its capabilities. We’ll show you both ways using the MJAndroid operations.

Inserting data into the database

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.

Example 8-1. Using 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();1
    map.put("employer_id", employer_id);
    map.put("title", title);
    map.put("description", description);
    try{
        getWritableDatabase().insert("jobs", null, map);2
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString());
    }
}

Here are some of the highlights of the code in Example 8-1:

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.

2

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.

Example 8-2. Using 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 = 1
        "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);2
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString());
    }
}

Here are some of the highlights of the code in Example 8-2:

1

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.

2

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.

Updating data already in the database

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.

Example 8-3. Using 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);1
    } catch (SQLException e) {
        Log.e("Error writing new job", e.toString());
    }
}

Here are some of the highlights of the code in Example 8-3:

1

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.

Example 8-4. Using 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.

Deleting data in the database

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.

Example 8-5. Using 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.

Example 8-6. Using 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());
    }
}

Content Providers

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

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

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

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

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

  • Create new records

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

  • Update records

  • Delete records if permitted

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

Note

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

Introducing NotePad

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

Activities

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

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

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

Database

The NotePad database is created with the following SQL statement:

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

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

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

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

Structure of the source code

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

AndroidManifest.xml

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

res/drawable/app_notes.png

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

res/layout/*.xml

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

res/values/strings.xml

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

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

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

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

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

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

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

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

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

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

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

Content Providers

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

Implementing a content provider

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

  1. Extend the ContentProvider class.

  2. Define the CONTENT_URI for your content provider.

  3. Create the data storage for your content.

  4. Create the column names for communication with clients.

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

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

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

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

  9. Define MIME types for any new data types.

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

Extend ContentProvider

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

public class NotePadProvider extends ContentProvider 

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

onCreate

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

getType

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

insert

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

query

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

update

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

delete

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

NotePadProvider class and instance variables

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

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

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

DATABASE_VERSION

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

NOTES_TABLE_NAME

The name of the notes table within the notes database.

mOpenHelper

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

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

private static HashMap<String, String> sNotesProjectionMap;
private static final UriMatcher sUriMatcher;
private static final int NOTES = 1;
private static final int NOTE_ID = 2;
...
static {
    sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
    sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);

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

The meanings of these variables follow:

sNotesProjectionMap

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

sUriMatcher

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

NOTES

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

NOTES_ID

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

Define CONTENT_URI

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

A CONTENT_URI consists of these parts:

content://

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

The authority

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

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

The path

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

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

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

    content://contacts/people/1

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

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

    In the first case, the MIME data type returned will be vnd.android.cursor.item/person, whereas in the second case, it will be vnd.android.cursor.item/phone.

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

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

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

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

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

Then, the CONTENT_URI itself is defined:

public static final class Notes implements BaseColumns {
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + 
      "/notes");
Create the data storage

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

@Override
public boolean onCreate() {
    mOpenHelper = new DatabaseHelper(getContext());1
    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:

1

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

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

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

Create the column names

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

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

Supporting binary data

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

Declare column specification strings

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

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

Implement the Cursor

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

Create data (insert)

Classes that extend ContentProvider must override its insert method. This method receives values from a client, validates them, and then adds a new row to the database containing those values. The values are passed to the ContentProvider class in a ContentValues object:

@Override
public Uri insert(Uri uri, ContentValues initialValues) {
    // Validate the requested uri
    if (sUriMatcher.match(uri) != NOTES) {
        throw new IllegalArgumentException("Unknown URI " + uri);
    }
    ContentValues values;
    if (initialValues != null)
        values = new ContentValues(initialValues);
    else
        values = new ContentValues();

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

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

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

    if (values.containsKey(NotePad.Notes.TITLE) == false) {
        Resources r = Resources.getSystem();
        values.put(NotePad.Notes.TITLE,r.getString(android.R.string.untitled));
    }

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

    SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    long rowId = db.insert(NOTES_TABLE_NAME, Notes.NOTE, values);
    if (rowId > 0) {
      Uri noteUri=ContentUris.withAppendedId(NotePad.Notes.CONTENT_URI,rowId);
      getContext().getContentResolver().notifyChange(noteUri, null);
      return noteUri;
    }
    throw new SQLException("Failed to insert row into " + uri);
}
Read/select data (query)

NotePadProvider must override the query method and return a Cursor containing the data requested. It starts by creating an instance of the 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;
}
Update data

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

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

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

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

The better approach is to pass a template as the where clause, something like "dogName=?". The question mark marks the location for the value of dogName, and the actual value is found in the whereArgs string array. The first question mark is replaced by the first value in the whereArgs string array. If there were a second question mark, it would be replaced with the second value, and so forth:

@Override
public int update(Uri uri,ContentValues values,String where,String[] whereArgs) {
    SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    int count;
    switch (sUriMatcher.match(uri)) {
    case NOTES:
        count = db.update(NOTES_TABLE_NAME, values, where, whereArgs);
        break;

    case NOTE_ID:
        String noteId = uri.getPathSegments().get(1);
        count = db.update(NOTES_TABLE_NAME, values, Notes._ID + "=" + noteId
              + (!TextUtils.isEmpty(where)?" AND ("+where+')':""), whereArgs);
        break;

    default:
        throw new IllegalArgumentException("Unknown URI " + uri);
    }

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

NotePadProvider’s delete method is very similar to the update method, but instead of updating the rows with new data, it simply deletes them:

@Override
public int delete(Uri uri, String where, String[] whereArgs) {
    SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    int count;
    switch (sUriMatcher.match(uri)) {
    case NOTES:
        count = db.delete(NOTES_TABLE_NAME, where, whereArgs);
        break;

    case NOTE_ID:
        String noteId = uri.getPathSegments().get(1);
        count = db.delete(NOTES_TABLE_NAME, Notes._ID + "=" + noteId
            + (!TextUtils.isEmpty(where)?" AND ("+where+')':""), whereArgs);
        break;

    default:
        throw new IllegalArgumentException("Unknown URI " + uri);
    }

    getContext().getContentResolver().notifyChange(uri, null);
    return count;
}
Updating AndroidManifest.xml

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

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

<provider android:name="NotePadProvider"
    android:authorities="com.google.provider.NotePad"
/>

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

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

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

Define MIME types

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

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

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

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

Your getType method must return the MIME type of the data at the given URI. In NotePad, the MIME types are stored as simple string variables, shown earlier in Structure of the source code. 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);
  }
}

Consuming a Content Provider

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

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

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.

NotesList Activity
Figure 8-1. NotesList Activity

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

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

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

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

URI of the content provider in which to insert the row

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

Data to insert

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

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

Read/query data

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

uri

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

projection

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

selection

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

selectionArgs

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

sortOrder

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

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

public class NotesList extends ListActivity {

    ...

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

    ...

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

        setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);2

        // 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();3
        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(),4
                PROJECTION, null, null, Notes.DEFAULT_SORT_ORDER);

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

Here are some of the highlights of the code:

1

Creates the projection, the first parameter to managedQuery. In this case, the array contains the note ID and title.

2

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.

3

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.

4

The managedQuery call, which returns a cursor.

5

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.

6

After you have created the Adapter, issue the ListActivity’s setListAdapter method to display the data from the Cursor on the screen.

Update data

To understand how to update data, we’ll take a look at the TitleEditor class. Because it’s small, looking at it in its entirety is instructive. Relatively few lines are needed to manipulate the content provider, and most of the function connects the user’s clicks to changes in the content provider. The user interaction uses basic manipulations of graphic elements, which were briefly introduced in Chapter 4 and will be fully discussed in Chapter 10 and subsequent chapters. The rest of this section prints the 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);1

    // 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);2

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

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

Here are some of the highlights of the code:

1

Finds the ContentView in the res/layout/title_editor.xml layout file, using the setContentView method.

2

Runs the managedQuery method to load results into a Cursor.

3

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();1
        values.put(Notes.TITLE, mText.getText().toString());2
        getContentResolver().update(mUri, values, null, null);3
    }
}

Here are some of the highlights of the code:

1

Creates a new ContentValues object to hold the set of values to pass to the ContentResolver.

2

Puts the column name and the new value of the column in the values object.

3

Stores the updated value by creating a ContentResolver and passing the URI and new vales to its update method.

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.

Delete data

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

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

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

Here are some of the highlights of the code:

1

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.

2

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.

3

Creates a ContentResolver and pass the URI to its delete method.

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

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