6.2
6.26.2 WritingWritingWritingWriting youryouryouryour ownownownown ContentContentContentContent ProviderProviderProviderProvider
To learn how to write a content provider, we're going to begin with the countries database from chapter 4. Copy the folder containing the SQLite1 project (or your own project if you've been writing code along with this book) into a new folder named “ContentProvider.” (In the code accompanying this book, there is already a ContentProvider folder for this chapter with this example).
To import a project into eclipse, first make sure that the project has been copied to the current workspace folder, then click File | Import... in the menu
In the Select dialog, choose Android/Existing Android Code Into Workspace as the import source, and click Next. Browse for the location of the source you wish to import:
If the import does not appear to work, make sure you've selected the folder you copied in, not a file within that folder. The ADT will import all the files in the project folder when it does its import.
Make sure the “Copy projects into workspace” box is selected, and click Finish.
The main difference between using SQLite directly and accessing SQLite through a content provider is in the way we use the activity to connect to the database. When using SQLite directly, we execute queries directly against the database, using the SQLiteOpenHelper object to get the database. When using a content provider, we execute queries against the content resolver object that binds to the provider. The role of the content provider is to define the
For this reason, implementing our own content provider requires that we override these CRUD methods. Recall that CRUD stands for Create, Retrieve, Update and Delete. Here are the methods we must implement, where they must be implemented, and which CRUD operation they represent:
query – This is a Retrieve operation, implemented in the content provider insert – This is an Update operation, implemented in the content provider update – This is an Update operation, implemented in the content provider delete – This is a Delete operation, implemented in the content provider onCreate – this is a Create operation, implemented in the SQLiteOpenHelper
onUpgrade – this is an Update operation (changes the schema), implemented in the SQLiteOpenHelper
In addition, the content provider also implements an onCreate method: we must establish a connection to the database here, using the SQLiteOpenHelper methods getReadableDatabase or getWritableDatabase. There are two further methods that must be overriden in the content provider implementation:
• onClose – typically just wraps a call to close on the database object
• getType – must return a String: this can be the proper mime type of the data in the database, or a string representation of the database uri itself. Should return a string representing the mime type if this application will export the content provider to other applications.
Since we're defining the behavior of the Retrieve, Update, and Delete methods in the content provider, we have full control of what the underlying data is. In other words, we could write these methods in such a way that they access flat files stored on the SD card or made calls to server side script to return XML (for example) from a server on the internet. This is what I meant earlier when I said that content providers provide a relational view of data: any data stored in any way anywhere can be accessed as if it were a local SQLite database via a content provider (provided that we implement the query, delete, insert, and update methods correctly).
For now, we'll stick with implementing a content provider that actually uses underlying SQLite data. Let's begin by creating a new class CountriesProvider that is a subclass of the ContentProvider class. Here's what the ContentProvider template gives us:
package package package packagecom.ebook.sqlite1; import import import importandroid.content.ContentProvider; import import import importandroid.content.ContentValues;
import import import importandroid.database.Cursor; import import import importandroid.net.Uri; public public public
public classclassclassclassCountriesProviderextendsextendsextendsextendsContentProvider {
@Override public
publicpublicpublic intintintintdelete(Uri arg0, String arg1, String[] arg2) { //TODOTODOTODOTODOAuto-generated method stub return
returnreturnreturn0; }
@Override public
publicpublicpublicString getType(Uri arg0) {
//TODOTODOTODOTODOAuto-generated method stub return
returnreturnreturn nullnullnullnull; }
@Override public
publicpublicpublicUri insert(Uri arg0, ContentValues arg1) { //TODOTODOTODOTODOAuto-generated method stub return
returnreturnreturn nullnullnullnull; }
@Override public
publicpublicpublic booleanbooleanbooleanbooleanonCreate() {
//TODOTODOTODOTODOAuto-generated method stub return
returnreturnreturn falsefalsefalsefalse; }
@Override public
publicpublicpublicCursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) {
//TODOTODOTODOTODOAuto-generated method stub return
returnreturnreturn nullnullnullnull; }
@Override public
publicpublicpublic intintintintupdate(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { //TODOTODOTODOTODOAuto-generated method stub
return returnreturnreturn0; }
Let's begin by defining some useful constants and other properties: public
public public
public classclassclassclassCountriesProviderextendsextendsextendsextendsContentProvider { private
privateprivateprivateSQLiteDatabasedatabase=nullnullnullnull; private
privateprivateprivateCountryDBcountriesHelper; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringAUTHORITY="com.ebook.sqlite1.CountryData"; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalUriCONTENT_URI= Uri.parse("content://"+AUTHORITY+"/"+ CountryColumns.CONTENT_PATH);
private
privateprivateprivate staticstaticstaticstatic finalfinalfinalfinal intintintintCOUNTRY_LIST= 1; private
privateprivateprivate staticstaticstaticstatic finalfinalfinalfinal intintintintCOUNTRY_ID= 2; private
privateprivateprivate staticstaticstaticstatic finalfinalfinalfinalUriMatcherURI_MATCHER;
public
publicpublicpublic staticstaticstaticstatic interfaceinterfaceinterfaceinterfaceCountryColumnsextendsextendsextendsextendsBaseColumns { public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalUriCONTENT_URI= CountriesProvider.CONTENT_URI; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringNAME="Name"; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringPOPULATION="Pop"; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringAREA="Area"; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringCONTENT_PATH="countries"; public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringCONTENT_TYPE= ContentResolver.CURSOR_DIR_BASE_TYPE+ "/ebook.countriesDB.countries";
public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringCONTENT_ITEM_TYPE= ContentResolver.CURSOR_ITEM_BASE_TYPE+ "/ebook.countriesDB.countries";
public public public
public staticstaticstaticstatic finalfinalfinalfinal String[] PROJECTION_ALL = {_ID, NAME, POPULATION,
AREA};
public
publicpublicpublic staticstaticstaticstatic finalfinalfinalfinalStringSORT_ORDER_DEFAULT=NAME+" ASC"; }
static staticstaticstatic{
URI_MATCHER=newnewnewnewUriMatcher(UriMatcher.NO_MATCH);
URI_MATCHER.addURI(AUTHORITY, CountryColumns.CONTENT_PATH,
COUNTRY_LIST);
URI_MATCHER.addURI(AUTHORITY, CountryColumns.CONTENT_PATH+"/#",
COUNTRY_ID); }
//. . .
There's actually quite a lot of new stuff here, but don't worry, we'll go thorough it a step at a time. First, we define a property database as our database object, then a property
countriesHelper as an instance of the CountriesDB class (our SQLiteOpenHelper class). Secondly, since we're defining a new content provider, we must supply an authority and a CONTENT_URI for this provider; we do that here. We get the table name for the uri from the CountryColumns object, defined here as being of type BaseColumns, which we will talk about shortly.
The next thing we do is define two constants COUNTRY_LIST and COUNTRY_ID. Recall that there are two forms of uri used when searching a content provider: one for all rows in a table and one for the data in the row with a specific ID. The COUNTRY_LIST constant corresponds to the first type of uri, and the COUNTRY_LIST to the second. We'll use these constants to determine what type of uri is being sent to the provider at runtime.
To aid in distinguishing the two types of uri, we declare a UriMatcher instance named URI_MATCHER, and define it later. The function of a UriMatcher is to list all expected forms of uri for the content provider. In this example, we list the two “normal” forms: a uri with an authority and a table only, and a uri with authority, table, and ID.
A BaseColumns is a class that groups information about the structure of the database into a single place. Here, we begin by providing the CONTENT_URI, and constants that represent the names of the columns in the table. There is also a column id that is not explicitly entered here: by default, the predefined constant BaseColumns._ID resolves to “_id.” CONTENT_PATH is given as the name of the table “countries,” as defined in the CountryDB class.
CONTENT_TYPE and CONTENT_ITEM_TYPE are defined as the strings that will be returned from the getType method, depending on the form of the uri request. Since we're not going to be publishing this content provider out to the world, we can simply provide strings here, but they should be appended to the existing value of the BaseColumns.CURSOR_ITEM_BASE_TYPE constant.
Finally, we provide a default sort order if none is specified in the query. In this case, the default sort order is “ORDER BY Name ASC” in terms of SQL syntax.
For further detail on the BaseColumns and UriMatcher classes, consult the documentation. In reality, learning by example with these two classes is best. They are simply helper classes that allow us to organize strings and other data so we can call upon it at need.
With the definition of these values out of the way, we can turn to the implementation of the various methods. Let's look at onCreate first:
public public public
public booleanbooleanbooleanbooleanonCreate() {
//TODOTODOTODOTODOAuto-generated method stub boolean
countriesHelper=newnewnewnewCountryDB(getContext(),"countries.db", nullnullnullnull); database=countriesHelper.getWritableDatabase();
if
ififif(database==nullnullnullnull) { retVal =falsefalsefalsefalse; }
if
ififif(database.isReadOnly()) { database.close(); database=nullnullnullnull; retVal =falsefalsefalsefalse; }
return
returnreturnreturnretVal; }
The goal of onCreate is to get the database property correctly set up. Here, we use getWritableDatabase on the countriesHelper object to return the database. We must check for two potential problems: the database might be null, or the underlying database may not be writeable. If either of these conditions holds, we want to return false. In the case of the database not being writeable, before setting the return value false, we should first close the database and set the property to null.
There are two schools of thought on the subject of returning data from a function: I personally hold that there should only be a single return statement from any function or method. This is why I use a var retVal to hold the return value until I am ready to return. You might see this sort of method implemented this way:
public public public
public booleanbooleanbooleanbooleanonCreate() {
//TODOTODOTODOTODOAuto-generated method stub
countriesHelper=newnewnewnewCountryDB(getContext(),"countries.db", nullnullnullnull); database=countriesHelper.getWritableDatabase();
if
ififif(database==nullnullnullnull) { returnfalsefalsefalsefalse; }
if
ififif(database.isReadOnly()) { database.close(); database=nullnullnullnull; returnfalsefalsefalsefalse; }
return returnreturnreturntrue; }
second method isslightly more efficient, because the method will return false immediately if either of the two check conditions are true, but this slight gain in clock ticks is unlikely to be noticed by the user (in this case). In the second if statement, virtually no time is gained, because of the close operation and assignment before the return. Ok, enough computer science theory.
In the case of onClose, we simply close the database connection: public
public public
public voidvoidvoidvoidonClose() { database.close(); }
We've added this method implementation to the content provider: it is not an inherited abstract function of ContentProvider. I find it useful to ensure that a content provider closes
its database connection when I'm done with it.
Now it's time to begin implementation of the update, delete, insert, and query methods. I personally change the names of the parameters in my implementations to be something more meaningful that arg0, arg1, etc. Here is query:
public public public
publicCursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //TODOTODOTODOTODOAuto-generated method stub
//get a new query builder (much simplifies the process !!) SQLiteQueryBuilder qBuilder =newnewnewnewSQLiteQueryBuilder(); //get the table and default sort order (if necessary):
qBuilder.setTables(CountryColumns.CONTENT_PATH); if
ififif(TextUtils.isEmpty(sortOrder)) {
sortOrder = CountryColumns.SORT_ORDER_DEFAULT; }
//check if query is for all columns or a specific column: switch
switchswitchswitch(URI_MATCHER.match(uri)) { case
casecasecaseCOUNTRY_LIST: //query is fine: break
breakbreakbreak; case
casecasecaseCOUNTRY_ID:
//wants a specific row:
qBuilder.appendWhere(CountryColumns._ID+" = "+ uri.getLastPathSegment());
break breakbreakbreak; default defaultdefaultdefault:
//invalid query... throw
uri); }
//now, we can query:
Cursor cur = qBuilder.query(database, projection, selection, selectionArgs,nullnullnullnull,nullnullnullnull, sortOrder);
//we might want to see changes:
cur.setNotificationUri(getContext().getContentResolver(), uri); return
returnreturnreturncur; }
The five parameters for the query method are the Uri, an array of column names (referred to as a projection), the WHERE clause, the WHERE clause arguments (if any), and the ORDER BY clause. Renaming these arguments makes much more sense in my opinion.
One extremely useful class when dealing with underlying SQLite data is SQLiteQueryBuilder. This class vastly simplifies construction of a query, so get in the habit of using it! Here, we get an instance of this class (qBuilder) and use it to set the table name, modify the where clause if necessary, and pass the query on to the database.
If the query method is called with a sortOrder parameter of null, we change the parameter to the default sort order, to ensure that the returned cursor will be sorted by country name. Then we use the UriMatcher object to distinguish which form of the Uri was passed to the query: if it is the standard table form, the query can stand as it is, but if a specific row ID is asked for, we must append that information to the WHERE clause. This is easily done by using the query builder's appendWhere method.
If the Uri is not in the standard or ID form, we throw an exception. This condition is a runtime error because no activity should be allowed to cal the content provider's query method with an improperly formed Uri!
The SQLiteQueryBuilder's query method is extremely robust. It accepts seven arguments: the database, the projection array, the WHERE clause, the WHERE arguments, a GROUP BY clause, a HAVING clause, and the ORDER BY clause. In overwhelmingly most situations (at least on mobile devices), we don't make use of the GROUP BY or HAVING statements, so nulls are passed here. The remaining parameters are passed through the query method after alterations (if necessary). This method returns a Cursor, and now we have our return value. Before returning this cursor, we notify the content resolver that there may have been a change, because SELECT queries can be written in such a way as to change the information in a database.
Study the implementation of the query method above. This will give you a good starting point for writing your own implementations of query in content providers. In fact, this method can stand “as is” for most any SELECT type query against a SQLite database,
provided that the Uris accepted are only of the standard two forms. Make sure you understand how the two forms of Uri are distinguished in this implementation! (the .match method of UriMatcher returns the constant value supplied in the third parameter of the addURI method for each of its Uris...):
static staticstaticstatic{
URI_MATCHER=newnewnewnewUriMatcher(UriMatcher.NO_MATCH);
URI_MATCHER.addURI(AUTHORITY, CountryColumns.CONTENT_PATH,COUNTRY_LIST); URI_MATCHER.addURI(AUTHORITY, CountryColumns.CONTENT_PATH+"/#",
COUNTRY_ID); }
The next method to implement (in terms of complexity, at least) is the delete method. Once again, we've changed the parameter names to more informative ones:
public public public
public intintintintdelete(Uri uri, String selection, String[] selectionArgs) { //TODOTODOTODOTODOAuto-generated method stub
int
intintintcount = 0; switch
switchswitchswitch(URI_MATCHER.match(uri)) { case
casecasecaseCOUNTRY_LIST:
count =database.delete(CountryColumns.CONTENT_PATH, selection, selectionArgs); break break break break; case
casecasecaseCOUNTRY_ID:
String strID = uri.getLastPathSegment();
String whereClause = CountryColumns._ID+" = "+ strID; if
if if
if(!selection.isEmpty()) {
whereClause +=" AND "+ selection; }
count =database.delete(CountryColumns.CONTENT_PATH, whereClause, selectionArgs); break break break break; default defaultdefaultdefault:
throw throw throw
throw newnewnewnewIllegalArgumentException("Unknown URI: "+ uri); }
if
ififif(count > 0) {
getContext().getContentResolver().notifyChange(uri,nullnullnullnull); }
return returnreturnreturncount; }
The delete method takes three parameters: the Uri, WHERE clause, and WHERE arguments. Again, we match the form of the incoming Uri: if it is the table only form, we just call delete
against the database, passing the table name, where clause, and arguments. But if a specific row is called for in the Uri, we need to change the where clause to delete only that row. If a WHERE clause is provided as an argument to the content provider's delete method, we also append that existing information. This may result in now rows being deleted, of course, which is not an exception. The only exception that might occur results from an invalid form of Uri being passed in: we handle this in the default case by throwing an IllegalArgumentException.
The database's delete method returns an integer indicating the number of rows that were deleted. If this integer is greater than zero, there has been a change in the data, and we must inform the content resolver of this change. Finally, we return the value of count.
The insert and update methods are very similar: both are supplied with a ContentValues
object in their parameter list. The function of an INSERT query is to add a new row to a database, UPDATE changes the content of one or more existing rows, if possible. The update method is slightly easier to implement, so let's tackle that one first:
public public public