28-Oct-08 (Created: 28-Oct-08) | More in 'Android Data Storage'

04 Understanding Android Content Providers

Content Providers

In android data sources are encapsulated through a concept called "content providers". This encapsulation is responsible for both retrieving data and also storing data. This abstraction is only required if you want to share data externally or between applications. Access to internal data to an application could use any mechanism that the application deems suitable. The available data storage/access mechanisms in Android include

  1. Preferences: A set of key value pairs that can be persisted to store application preferences
  2. Files: Files internal to applications and can be stored on a removable storage medium.
  3. SQLite: SQLite database. Each database is private to the package that creates that database
  4. Content Providers: An inter application data sharing facility
  5. Network: Store the data externally through the internet

This chapter primarily focusses on how to use existing providers and create new providers. Although the mechanism could use any kind of storage strategy the prevalent choice is to use SQLite database that is resident on the device.

Android Built in Providers

Android provides a number of providers out of the box. These include


Browser
CallLog
Contacts
   people
   phones
   photos
   groups
MediaStroe
   audio
      albums
      artists
      Geners
      playlists
   images
      thumbnails
   video
Settings

Most of these providers are backed by a local data store managed using SQLLite. These databases typically have an extension of .db and accessible only from the implementation package. Any access outside of that package have to go through the provider interfaces.

How to explore databases on the emulator and available devices

As many content providers in Android use SQLite databases, it is worthwhile examining these databases for their tables and also the data in those tables. Android provides tools to examine these databases.

One of the tools is a remote shell on the device that allows executing commandline sqlite tools against a specified database. We will learn in this section how to use this command line utility to examine built in android databases. Many of the tools required for this exercise can be found in the sub directory:


\android-sdk-install-directory\tools 

Android uses a command line tool called "Android Debug Bridge" (adb) to do many things with android devices including the emulator. This tool is available as


tools\adb.exe

adb is a special tool in the android tool kit that most other tools go through "adb" to get to the device. You can see the many options and commands that you can run with adb by typing at the command line:


adb help

You can also visit the following url for its many options


http://code.google.com/android/reference/adb.html

One of the things adb can do is to open a shell on the device. This shell is essentially a unix "ash" albeit with a limited command set. For example you can do "ls" but "find", "grep", and "awk" are not availalbe in the shell. You can see the available command set by doing the following in the shell


ls /system/bin

In the current release this will bring up the following commands


dumpcrash
am
dumpstate
input
itr
monkey
pm
svc
ssltest
debuggerd
dhcpcd
hostapd_cli
fillup
linker
logwrapper
telnetd
ping
sh
hciattach
sdptool
logcat
servicemanager
dbus-daemon
debug_tool
flash_image
installd
dvz
hostapd
htclogkernel
mountd
qemud
radiooptions
toolbox
hcid
chmod
date
dd
cmp
cat
dmesg
df
getevent
getprop
hd
id
ifconfig
insmod
ioctl
kill
ln
log
lsmod
ls
mkdir
iftop
mkdosfs
mount
mv
notify
netstat
printenv
reboot
ps
renice
rm
rmdir
rmmod
sendevent
schedtop
route
setprop
sleep
setconsole
smd
stop
top
start
umount
vmstat
wipe
watchprops
sync
netcfg
dumpsys
service
playmp3
sdutil
rild
dalvikvm
dexopt
surfaceflinger
app_process
mediaserver
system_server

You can use the "ls" command to figure out what databases are on the emulator. However for "adb" to work you have to have an emulator running. You can find out runnning devices or emulators by typing in the following at the command line:


adb devices

This will indicate if there are any devices running including the emulator. If the emulator is not runnng, you can start the emulator by typing


\tools\emulator.exe

This will start the emulator. Once the emulator is up and running you can test again for running devices by typing


\tools\adb.exe devices

Now you should see a print out that looks like:


List of devices attached
emulator-5554 device 

Now if you type


\tools\adb.exe shell

adb.exe will open up an "ash" on the emulator. In this shell if you type


ls -l

You will see a list of root level directories and files. To know what databases are there, the directory of interest to us is


ls /data/data

This directory will have the list of packages on the device. Let us look at an example


ls /data/data/com.android.providers.contacts/databases

This will list a database file called


contacts.db

Contacts.db is an sqlite database. if there was a "find" command in the included "ash" we would have been able to look at all the "*.db" files. But there is not good way to do this with "ls" alone. Nearest thing you can do is:


ls -R /data/data/*/databases

In the current release of the emulator you will notice that it has the following databases


alarms.db
contacts.db
downloads.db
internal.db
settings.db
mmssms.db
telephony.db

You can invoke sqlite3 on one of these databases inside the adb shell by typing in


#sqlite3  /data/data/com.android.providers.contacts/databases/contacts.db

The starting "#" sign is the prompt of the "adb shell" tool, which means you have to have the adb shell running before using the sqlite3 command line tool.

you can exit sqlite3 by typing


.exit

Notice that the prompt for adb is a "#" and the prompt for sqlite3 is a greater-than sign. You can read about many sqlite3 commands by visiting


http://www.sqlite.org/sqlite.html

However I will list a few important commands here so that you don't have to make a trip to the web. You can see a list of tables by typing


sqlite3> .tables 

This command is a shortcut for


SELECT name FROM sqlite_master 
WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%'
UNION ALL 
SELECT name FROM sqlite_temp_master 
WHERE type IN ('table','view') 
ORDER BY 1

As you could have guessed the table "sqlite_master" is a master table that keeps track of tables and views in the database. The following command line prints out a create statement for a table called people in the contacts.db


.schema people

This is one way to get at the column names of a table in sqlite. This will also print out the column data types. While working with content providers it is important to note these column types as access methods depend on the column type.

However it is pretty tedious to humanly parse through this long create statement just to know the column names and their types. Luckily there is a way around. You can "pull" down the contacts.db to your local box and then use any number of sqlite3 gui tools to examine the database. You can do the following to pull down the contacts.db file


adb pull  /data/data/com.android.providers.contacts/databases/contacts.db /somelocaldir/contacts.db

I have used a free download of sqliteman and seem to work ok. I did experience a few crashes but seem to work oveall.

Some SQLLite SQL examples

The following sample sql statements could help you to navigate through the sqlite databases quickly. You can use these as a quick reference.


salite>.headers on

select * from table1;

select count(*) from table1;

select col1, col2 from table1;

select distinct col1 from table1;

select count(col1) from (select distinct col1  from table1);

select count(*), col1 from table1 group by col1;

//regular inner join
select * from table1 t1, table2 t2
where t1.col1 = t2.col1;

//left outer join
//Give me everything in t1 even though there are no rows in t2 
select * from table t1 left outer join table2 t2
on t1.col1 = t2.col1
where ....

Architecture of Content Providers

So far we know what content providers are and how to explore existing content providers through android tools. Let us now examine some of the architectural under pinnings of Content providers and how these content providers relate to other data access facilities in the industry.

Content providers expose their data to their clients through a url. This is similar to a website exposing its content through urls. In that sense these content provider uris are like web services exposing data. Overall, content providers approach have parallels to:


websites
REST
Webservices
Stored Procedures

Each Content provider on a device registers itself like a website with a "domanin name lik string" and a set of uris. Here are two examples of providers registering themselves in androidmanifest.xml:


<provider android:name=".app.SearchSuggestionSampleProvider"
        android:authorities="com.example.android.apis.SuggestionProvider" />
        
<provider android:name="NotePadProvider"
   android:authorities="com.google.provider.NotePad"
/>

"Authority" is like a domain name for this content provider. Given the above "authority" registration these providers will honor urls starting with that authority prefix


content://com.example.android.apis.SuggestionProvider/
content://com.google.provider.NotePad/

Content providers also provide REST like URLs to retrive or manipulate data. Following the registration above, the uri to identify a "directory" or a collection of notes in the NotePadProvider database is


content://com.google.provider.NotePad/Notes

The url to identify a specific note is


content://com.google.provider.NotePad/Notes/#

where # is the id of a particular note. Here are some additional examples of Uris some data providers accept


content://media/internal/images
content://media/external/images
content://contacts/people/ 
content://contacts/people/23

Content providers exhibit characteristics of a web service as well. A content provider through its Uris expose internal data as services. However the output from the url of a content provider is not typed data as is the case for a SOAP based web service call.

The Uris of a content provider are also similar to the names of stored procedures in a database. Stored procedures present a service based access to the underlying relational data. However unlike a stored procedure the input to the service call in a content provider is typically embedded in the Uri itself. Verymuch like stored procedures, the Uri calls against a content provider returns a cursor.

Structure of Uris

So we said that a content provider acts like a web site which will respond to incoming uris. So to retrieve data from a content provider, all you have to do is invoke a uri to receive data. Indeed that is the case. It is just that the retrieved data is in the form of a set of rows and columns represented by an android Cursor object.

Content uris in Android has a general structure that looks like this


content:///

An example is


content://com.google.provider.NotePad/notes/23

All these Uris start with "content:", followed by a unique identifier for the provider called "Authority". In the example above "com.google.provider.NotePad" is the authority portion of the uri. This section of the uri is used to locate the provider in the provider registry.

"/notes/23" is the path section of the uri that is specific to each provider. It is the responsibility of the provider to document these uris clearly. Normally this is done by declaring constants in some class in that providers package. Further more the first portion of the path may be pointing to a collection of objects. For example "/notes" indicates a collection or a "directory" of notes. Where as "/23" points to a specific note item.

Given this Uri a provider is expected to retrieve rows identified by this Uri. The provider is also expected to alter content at this uri using any of the state change methods: insert, update, and delete.

Structure of Mime Types

Just like websites return the mime type for a given url, a content provider has an added responsibility to return the mime type for a given Uri. The idea of mime types are very similar. You ask a provider what the mime type is for a given uri that it supports and the provider returns a two part string identifying its mime type following the standard web mime conventions.

The MIME type for each content type has two forms: one for a specific record, and one for multiple records.

For a single record it will look like


vnd.android.cursor.item/vnd.yourcompanyname.contenttype

For a collection of records or rows


vnd.android.cursor.dir/vnd.yourcompanyname.contenttype

Examples:


//One single note
vnd.android.cursor.item/vnd.google.note

//A collection or a directory of notes
vnd.android.cursor.dir/vnd.google.note

Mime types are extensively used in Android especially in "Intents" where the system figures out what activity to invoke based on the mime type of data. Mime types are invariably derived from their Uris through content providers.

Reading data using Uris

We have gathered so far the basics of content providers to proceed to retrieve data from a content provider using Uris. As a website is prone to allow a number of different urls, all based at a certain root url, a content provider also allows a number of REST Uris. Because the Uris defined by a content provider are unique or specific to that provider it is important that these Uris are documented and available for clients to see and then call. The providers that come with Android makes this job easier by defining constants representing these Uri strings.

Consider the following three Uris defined by helper classes in the android SDK


MediaStore.Images.Media.INTERNAL_CONTENT_URI 
MediaStore.Images.Media.EXTERNAL_CONTENT_URI 
Contacts.People.CONTENT_URI 

The equivalent textual Uri strings would be:


content://media/internal/images
content://media/external/images
content://contacts/people/ 

MediaStore provider defines two Uris and Contacts provider defines one Uri. Given these Uris, the code to retrieve a single row of contact will look like this


Uri peopleBaseUri = Contacts.People.CONTENT_URI;
Uri myPerson = peopleBaseUri.withAppendedId(Contacts.People.CONTENT_URI, 23);

//Query for this record.
//managedQuery is a method on Activity class
Cursor cur = managedQuery(myPerson, null, null, null);

Notice how the Contacts.People.CONTENT_URI is predefined as a constant in People class. In this example, the code is taking the root uri and then adding a specific person id to the uri and then making a call to the managedquery method.

As part of this managed query, against this URI, it is possible to specify sort order, which columns to select, and a where clause.

A content manager should list which columns it supports by implementing a set of interfaces or by listing the column names as constants. Later in this section you can also learn how to examine databases on a device and discover their column names and types if needed. However the class that is defining constants for columns should also make the column types clear.

The method Activity.managedQuery(...) returns a managed cursor that knows how to unload itself when the application pauses and requerying itself when the application resumes.

Lets extend the example above to retrive a specific list of columns and not all from the People provider


// An array specifying which columns to return. 
string[] projection = new string[] {
    People._ID,
    People.NAME,
    People.NUMBER,
};

// Get the base URI for People table in Contacts content provider.
// ie. content://contacts/people/
Uri mContacts = People.CONTENT_URI;  
       
// Best way to retrieve a query; returns a managed query. 
Cursor managedCursor = managedQuery( mContacts,
                        projection, //Which columns to return. 
                        null,       // WHERE clause
                        People.NAME + " ASC"); // Order-by clause.

Notice how a projection is merely an array strings representing column names. So unless we know what these columns are it is hard to create a projection. You should look into the same class that provides the URI to look for these column names, in this case the "People" class. Let us take a look at the other column names on this class for a second:


CUSTOM_RINGTONE
DISPLAY_NAME
LAST_TIME_CONTACTED
NAME
NOTES
PHOTO_VERSION
SEND_TO_VOICE_MAIL
STARRED
TIMES_CONTACTED

It is also important to note that in a database like "contacts" there are several tables. Each table is represented by a class. Let us take a look at the package "android.providers.contacts"

This package has the following classes or interfaces


ContactMethods
Extensions
Groups
Organizations
People
Phones
Photos
Presence
Settings

Each of these classes represent a table name in contacts.db database. Each table is responsible for describing its own uri structure. There is also a corresponding "Columns" interface defined for each class identifying the column names. Some examples are PeopleColumns, PhotoColumns etc.

Coming back to the Cursor that is returned it contains zero or more records. Column names, order, and type are provider specific. However every row returned has a default column called _id representing a unique id for that row.

Using the Cursor object

A few things to know before you access a cursor are

  • A cursor is a collection of rows
  • You need to use moveToFirst() as the cursor is positioned before the first row
  • You need to know the column names
  • You need to know the column types
  • All field access methods are based on column number. You have to convert column name to a column number first
  • The cursor is a random cursor (you can move forward and backwards)
  • Because the cursor is a random cursor you can also ask a cursor for a row count

You will navigate through the cursor as follows


if (cur.moveToFirst() == false)
{
   //no rows empty cursor
   return;
}
//The cursor is already positioned at the begining of the cursor
//let's access a few columns
int nameColumnIndex = cur.getColumnIndex(People.NAME);
String name = cur.getString(nameColumnIndex);

//let's now see how we can loop through a cursor

while(cur.moveToNext())
{
   //cursor moved successfully
   //access fields
}

The assumption here is that the cursor has been positioned before the first row. To know where the cursor is you have the following methods:


isBeforeFirst()
isAfterLast() 
isClosed()

Using one of these methods you can do the following


for(cur.moveToFirst();!cur.isAfterLast();cur.moveToNext())
{
   //access methods
}

You can use the following method to find out the number of rows in a cursor


getCount()

Here is a more wholistic example


for(cur.moveToFirst();!cur.isAfterLast();cur.moveToNext())
{
   int nameColumn = cur.getColumnIndex(People.NAME); 
   int phoneColumn = cur.getColumnIndex(People.NUMBER);
   
   String name = cur.getString(nameColumn);
   String phoneNumber = cur.getString(phoneColumn);
}

Working with the where clause

To the casual eye the provider architecture seem to be closely tied to SQLite or a relational model. It is in fact a more abstract in nature centered around "uris" and "row sets". A "row set" although not an android term represents a collection of rows where each row is further broken down into a set of columns. Each row can further act as an input for another uri which will bring back its own dependent and independent row sets. And this can continue as deep as necessary.

For example a person uri in a contacts database is an independent uri because you can get to a set of persons with out having any parent. Where as phone numbers in the database are tied to a person. So a phone uri is dependent on a person uri. This is also how tables are looked upon as dependent or independent in a database as well. However this dependency is an abstract concept and doesn't necessarily assume a relational structure.

Each provider can be thought of as providing a set of dependent or indpendent uris with their own set of rows and columns. These rows and columns do not have to match with how the underlying data store such as a database defines them. However it will be convenient to have them that way if there is an underlying database. Even when there is a relational database underneath you may want to define your columns differently. For instance say you have two or three tables that you want to join them into a view. Then you can define that view as your logical table and expose those columns as your logical columns with a predefined where clause. In this mode you can imagine a "stored procedure" being defined as a uri with arguments. The uri arguments could match the stored procedure arguments.

Query statements on a provider has multiple ways of passing a constraint clause (or where clause).

  1. through the REST uri
  2. through a combination of a string clause and a set of replacable string array arguments
  3. as string based positional arguments
  4. as a set of string based key value pairs

The last two methods are really finagling the where clause provision and will be non standard for a provider if used.

Passing where clauses through REST uri

This is probably a preferred method to specify a where clause or more importantly a "selection". Lets take a look at an example


//Retrieve a note id from the incoming uri that looks like
//content://.../notes/23
int noteId = uri.getPathSegments().get(1);

//ask a query builder to build a query
//specify a table name
queryBuilder.setTables(NOTES_TABLE_NAME);

//use the noteid to put a where clause
queryBuilder.appendWhere(Notes._ID + "=" + );

Notice how the id of a note is extracted from the Uri. The ultimate where clause is constructed using this extracted parameter from the uri. if the incoming uri is


content://com.google.provider.NotePad/notes/23

Then the equivalent select statement would have been


select * from notes where _id == 23

You will use the classes "Uri" and "UriMatcher" to identify Uris and extract parameters from Uris. QueryBuilder is a helper class that allows you to construct sql queries to be executed by SQLiteDatabase on an sqlite database instance.

Using open ended whereclauses

It is time we take a closer look at the "query" api of the ContentResolver class. Let us look at its signature


public final Cursor query(Uri uri, 
   String[] projection, 
   String selection, 
   String[] selectionArgs, 
   String sortOrder) 

Notice the argument "selection". This is of type String. This "selection" string represents a filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI. In the "selection" string you may include ?s, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.

Because there are two ways of specifying a where clause it is not very clear how a provider will have used these where clauses or which where clause takes precedence or if both where clauses are utilized. For example the incoming uri


content://com.google.provider.NotePad/notes/23

Can be queried using any one of the following queries


query("content://com.google.provider.NotePad/notes"
,null
,"_id=?"
,new String[] {23}
,null);

or

query("content://com.google.provider.NotePad/notes/23"
,null
,null
,null
,null);

It would be worth noticing how some providers implement a query for a given uri. Let us take a look to see how NotesProvider implements or treats the where clause


@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 the cursor what uri to watch, so it knows when its source data changes
   c.setNotificationUri(getContext().getContentResolver(), uri);
   return c;
}

Notice how a single method is invoked for all supported URIs by a provider. This method should distinguish which uri it is and then allocate a table for that uri and then populate the columns and the where clause. In this case, the passed in note id is used as a where clause and also any passed in where clause on top of it. In fact same sql statement is used for both in coming uris except for the different where clause treatment.

Furthermore, the query builder that is used to formulate the query gets passed the where clause twice. Once through the "selection" string and also once explicitly through the "append" method. Here is what the documentation says:


public void appendWhere(CharSequence inWhere) 

This method appends a chunk to the WHERE clause of the query. All chunks appended are surrounded by parenthesis and ANDed with the selection passed to


query(SQLiteDatabase, String[], String, String[], String, String, String). 

The final WHERE clause will look like:


WHERE (<append chunk 1><append chunk2>) 
AND (<query() selection parameter>) 

Keep the above append syntax in mind when you are joining two where clauses as above.

Inserting records

Android uses a structure called ContentValues to insert records. ContentValues is a dictionary of key value pairs much like column names and their values. You insert records by first populating a record into "ContentValues" and then asking the ContentResolver to insert that record using a Uri. Here is an example of populating a single row of "People" in ContentValues in preparation for an insert


//construct an empty dictionary
ContentValues values = new ContentValues();

//fill up the name column
values.put(Contacts.People.NAME, "New Contact");

//fill up the favorites column
//1 = the new contact is added to favorites
//0 = the new contact is not added to favorites
values.put(Contacts.People.STARRED,1);

//values object is now ready to be inserted

You can get a reference to the ContentResolver by asking the Activity class


ContentResolver contentResolver = activity.getContentResolver();

Now all you need is a URI to tell the contentResolver to insert the row. These Uris' are defined in a class corresponding to the People table. So this class should have the Uri that we need to insert a record. This Uri is the same Uri that you would have used to retrieve a collection of People. Lets take a look at what this defintion may look like


Contacts.People.CONTENT_URI

We can take this URI and the ContentValues we have and then make a call to insert the row


Uri uri = contentResolver.insert(Contacts.People.CONTENT_URI, values);

An important note here is that the call above returns a "uri" pointing to the newly inserted record of the person. So this returned URI would match the structure:


Contacts.People.CONTENT_URI/new_id

In the class People you will notice that there are a series of nested classes:


package: android.provider
Contacts
   ...
   People
      Phones
      email
      ....

This hierarchy also reflects the table structure and as well as the Uri structure. we can continue to add the phone number for that person. We use a similar approach. We will populate ContentValues object with column values for that row and then ask the content resolver to insert it. However like before we need to know what this Uri is. This Uri will look like


Contacts.People.CONTENT_URI/new_id/phones

But again we want to make use of the pre-defined constants for the URIs. This time the URI will come from Phones. Interestingly if you look at the class Contants.People.Phones, you will notice that there is no CONTENT_URI defined for this class. This is because, being a dependent table and a nested class it will use the Uri defined by its parent table "People" and then just append an appropriate dependent table name after that. This name is indicated by the constant


Contacts.People.Phones.CONTENT_DIRECTORY

The word DIRECTORY indicates that this is the collection of "Phones" as opposed to an individual item. The class Uri has helper methods to construct the URI for the collection of phones for a given "person". Here is how that is done


Uri phoneUri = Uri.withAppendedPath(specificPersonUri, Contacts.People.Phones.CONTENT_DIRECTORY);

The end result would have been a uri that will look like


Contacts.People.CONTENT_URI/new_id/phones

Essentially "phones" which is the value of Phones.CONTENT_DIRECTORY is just added to the end of the uri that was returned from inserting the person in the people table. This pattern will continue for adding other records to the person dependent tables. But first let us see the code for inserting a "Phone" record for a "Person"


phoneUri = Uri.withAppendedPath(uri, Contacts.People.Phones.CONTENT_DIRECTORY);
values.clear();
values.put(Contacts.Phones.TYPE, Phones.TYPE_MOBILE);
values.put(Contacts.Phones.NUMBER, "1233214567");
contentResolver.getContentResolver().insert(phoneUri, values);

Notice how the ContentValues is cleaned up and repopulated for the next record. Following the same pattern the code for adding an email is


//Add Email
emailUri = Uri.withAppendedPath(uri, ContactMethods.CONTENT_DIRECTORY);
values.clear();
//ContactMethods.KIND is used to distinguish different kinds of
//contact data like email, im, etc. 
values.put(ContactMethods.KIND, Contacts.KIND_EMAIL);
values.put(ContactMethods.DATA, "[email protected]");
values.put(ContactMethods.TYPE, ContactMethods.TYPE_HOME);
contentResolver.insert(emailUri, values);

Dictionary of objects to assist filtering

Because providers have an extra layer between databases and clients they have an opportunity to interpret in coming uris and selection arguments. Theoretically they can do any thing they wish with these values. For example a provider may choose to ignore selection (or where clause) arguments completely or partially based on a given uri.

For example, you could design a provider that would assume a "selection" string as a set of column names separated by commas and then use the selection args string array as values to those column names simulating a key/value pair idea.

Here is an example


String selection="column1,column2";
String []columnValues = new String[] {"value1","4"};

Or you could just ignore the "selection" argument and use the selection args string array as a set of positional parameters.

However, in both these cases you will loose the flexibility of an open ended where clause. But again, having a REST like provider wrapper on an underlying database would invariably imply some restrictions just like a service layer on a database would impose some restrictions.

Adding a file to a content provider

Android introduces another novelty for managing files through databases. If you want to insert a file into a database managed by a provider Android uses a convention where a reference to the filename is saved in a record with a reserved column name of "_data". Android expects you to store the metadata of a file in a record using a Uri. In reply provider will return a Uri pointing to the file. The Uri is then passed to the openOutputStream() method to get a file handle to write to. The Uri does not point to the filename directly. Uri points to a record in the database. This record should have the "_data" in it.

Here is some example code from Android SDK documentation


// Save the name and description in a map. Key is the content provider's
// column name, value is the value to save in that record field.
ContentValues values = new ContentValues(3);
values.put(MediaStore.Images.Media.DISPLAY_NAME, "road_trip_1");
values.put(MediaStore.Images.Media.DESCRIPTION, "Day 1, trip to Los Angeles");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");

// Add a new record without the bitmap, but with the values.
// It returns the URI of the new record.
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

try {
    // Now get a handle to the file for that record, and save the data into it.
    // sourceBitmap is a Bitmap object representing the file to save to the database.
    OutputStream outStream = getContentResolver().openOutputStream(uri);
    sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
    outStream.close();
} catch (Exception e) {
    Log.e(TAG, "exception while writing image", e);
}

Updates and Deletes

So far we have talked about query and insert. Updates and Deletes are fairly straight forward. Update is especially similar to insert where changed column values are passed through a ContentValues structure that can also use open ended where clause.

Almost all the calls from ContentResolver are directed eventually to the provider class. Knowing how a provider implements each of these methods gives a clue as to how those methods are used by a client.

So we will postpone covering these two methods until we cover implementing ContentProviders.

ContentProvider abstract class

So far we have discussed how to request data from a provider but haven't discussed how to write one. To be a content provider you have to extend the ContentProvider and implment the following methods


query
insert
update
delete
getType

Most methods take Uri as an input argument. The first part of the Uri should match the "authority" registered in the manifest file. Update methods also take a set of key value pairs defined by the class ContentValues class.

All these methods are invoked by an implmentation of an abstract class called ContentResolver. Under the covers android uses an implementation of this class to direct all these methods to an appropriate content provider based on the Uri. In a sense the Uri becomes the key to locate the provider and then call the provider. Activity class has a method to obtain an implmentation of the ContentResolver class.

The steps of implementing a Content Provider class involve

  1. Extend the abstract class ContentProvider
  2. Implement methods query, insert, update, delete, getType
  3. Declare Uris supported by this provider
  4. Declare column names after deriving from basecolumns
  5. Register provider in the manifest file

Let us examine these steps in some detail

Creating a content provider

Start with extending the ContentProvider

Example:


public class NotePadProvider extends ContentProvider 
{
}

Declare a public CONTENT_URI.

Example:


public static final String AUTHORITY = "com.google.provider.NotePad";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");

Define the column names that you will return to your clients. If you are using an underlying database, these column names are typically identical to the SQL database column names they represent. You should include an integer column named _id to define a specific record number. If using the SQLite database, this should be type INTEGER PRIMARY KEY AUTOINCREMENT. The AUTOINCREMENT descriptor is optional, but by default, SQLite autoincrements an ID counter field to the next number above the largest existing number in the table.

Document the data type of each field. Remember that file fields, such as audio or bitmap fields, are typically returned as string path values.

Example:


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

    /**
     * Notes table
     */
    public static final class Notes implements BaseColumns {
        // This class cannot be instantiated
        private Notes() {}

        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 DEFAULT_SORT_ORDER = "modified DESC";

      //Columns
        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";
    }
}

Notice how columns are derived from BaseColumns as well.

If you are exposing byte data, such as a bitmap file, the field that stores this data should actually be a string field with a content:// URI for that specific file. This is the field that clients will call to retrieve this data. The content provider for that content type (it can be the same content provider or another content provider for example, if you're storing a photo you would use the media content provider) should implement a field named _data for that record. The _data field lists the exact file path on the device for that file.

This field is not intended to be read by the client, but by the ContentResolver. The client will call ContentResolver.openOutputStream() on the user-facing field holding the URI for the item (for example, the column named photo might have a value content://media/images/4453). The ContentResolver will request the _data field for that record, and because it has higher permissions than a client, it should be able to access that file directly and return a read wrapper for that file to the client.

Once the necessary columns and uris are defined, proceed to mplement query, update, delete, insert, and getType methods. You also have the option of creating a new database if the database doesn't exist already. Here is how NotepadProvider does this


public class NotePadProvider extends ContentProvider {

    private static final String TAG = "NotePadProvider";

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


    /**
     * This class helps open, create, and upgrade the database file.
     */
    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 oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS notes");
            onCreate(db);
        }
    }

    private DatabaseHelper mOpenHelper;

    @Override
    public boolean onCreate() {
        mOpenHelper = new DatabaseHelper(getContext());
        return true;
    }
...
}

Implementing getType


    @Override
    public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
        case NOTES:
            return Notes.CONTENT_TYPE;

        case NOTE_ID:
            return Notes.CONTENT_ITEM_TYPE;

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

Implementing Query


public class NotePadProvider extends ContentProvider {
   ...

    private static HashMap<String, String> sNotesProjectionMap;
    private static final int NOTES = 1;
    private static final int NOTE_ID = 2;
    private static final UriMatcher sUriMatcher;

    @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 the cursor what uri to watch, so it knows when its source data changes
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }
...
}

Implementing Insert


    @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);
    }

Implementing delete


    @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;
    }

Implementing update


    @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;
    }

Using UriMatcher to figure out the Uris

Almost all methods in a ContentProvider is overloaded with respect to the Uri. Take a "query" method for example. Same method is going to be called irrespective of the number of Uris supported by a provider. It is upto the method to know which Uri is being requested for. Android provides utility class called UriMatcher to help with this.

The way this works is you tell an instance of UriMatcher what kind of Uri patterns you can expect. For each pattern you will also associate a unique number. Once these patterns are registered, you can then ask the urimatcher if the incoming Uri matches a certain pattern.

For the notepad provider there are two patterns for the Uris. One for a collection of notes and one for a single note. Code below registers both these patterns with a UriMatcher. It allocates "1" (NOTES) to the first Uri and a "2" (NOTE_ID) for the second uri.


    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
        sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
    }

With this registration in place see how UriMatcher is used in the query method implementation


switch (sUriMatcher.match(uri)) {
   case NOTES:
   case NOTE_ID:
   default:
      throw new IllegalArgumentException("Unknown URI " + uri);
}

Notice how the "match" method returns the same number that was registered earlier back. The constructor of the UriMatcher takes an integer to use for the root Uri. UriMatcher returns this number if there are no path segments on the url and there is no authority on the url. UriMatcher also returns NO_MATCH when the patterns don't match. It is possible to construct a UriMatcher with no root matching code. In that case internally it initializes it to NO_MATCH. So you could have written the code above as


    static {
        sUriMatcher = new UriMatcher();
        sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
        sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
    }

Using Projection Maps

Because a provider acts like an intermediary between an abstract set of columns and a real set of columns in a database it is possible that these are different. While constructing queries one has to map between the whereclause columns that a client specifies and the real database columns. To help with that SQLiteQueryBuilder takes a map of strings to get this mapping. Most times these are same.

Here is what the documentation says about this mapping method


public void setProjectionMap(Map columnMap) 

Sets the projection map for the query. The projection map maps from column names that the caller passes into query to database column names. This is useful for renaming columns as well as disambiguating column names when doing joins. For example you could map "name" to "people.name". If a projection map is set it must contain all column names the user may request, even if the key and value are the same.

Here is how the NoteProvider sets this up


private static HashMap<String, String> sNotesProjectionMap;
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);

And then this variable sNotesProjectionMap is used by the query builder as follows:


queryBuilder.setTables(NOTES_TABLE_NAME);
queryBuilder.setProjectionMap(sNotesProjectionMap);

Registering the Provider

Add a provider tag to AndroidManifest.xml, and use its authorities attribute to define the authority part of the content type it should handle. For example, if your content type is content://com.google.provider.NotePad/notes to request a list of all notes, then authorities would be com.google.provider.NotePad. Set the multiprocess attribute to true if data does not need to be synchronized between multiple running versions of the content provider.

Example:


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

Important classes while using or creating content providers

Activity: Gives access to a ContentResolver class which acts like a router to direct data access/update Uris. You will use getResolver() method to gain access to the underlying implementation of the ContentResolver.

ContentResolver: An abstract router class for directing uris to respective providres. You will use this class for querying, inserting, deleting, or updating data in a content provider based on the Uri.

Uri: A typed class representation of textual Uri. It has methods to parse uri strings into the typed Uri and also has methods to retrieve segments of this uri for parameters. Uris in android are typically REST based.

UriMatcher: A utility class used by a content provider to quickly identify in coming Uris based on a pattern.

SQLiteDatabase: A wrapper class for the underlying sqlite database. Has all the typical operations for manipulating a database including transactions. The method "rawQuery" is similar to a jdbc query call.

SqliteQueryBuilder: A convenience class to gradually build an sql query to be executed by an sqlitedatabase.

Cursor: An abstract represenation of rows and columns. Android cursors are both forward and backward.

ContentProvider: An abstract class requiring the implementer to implement query, insert, delete, update, and getType based on incoming uris.

SQLiteOpenHelper: A helper class to manage database creation and version management. You create a subclass implementing onCreate(SQLiteDatabase), onUpgrade(SQLiteDatabase, int, int) and optionally onOpen(SQLiteDatabase), and this class takes care of opening the database if it exists, creating it if it does not, and upgrading it as necessary. Transactions are used to make sure the database is always in a sensible state.