Chapter 7

Content Providers

WHAT YOU WILL LEARN IN THIS CHAPTER

  • What are content providers?
  • How to use a content provider in Android
  • How to create and use your own content provider

In the previous chapter, you learned about the various ways to persist data — using shared preferences, files, as well as SQLite databases. While using the database approach is the recommended way to save structured and complex data, sharing data is a challenge because the database is accessible to only the package that created it.

In this chapter, you will learn Android’s way of sharing data through the use of content providers. You will learn how to use the built-in content providers, as well as implement your own content providers to share data across packages.

SHARING DATA IN ANDROID

In Android, using a content provider is the recommended way to share data across packages. Think of a content provider as a data store. How it stores its data is not relevant to the application using it; what is important is how packages can access the data stored in it using a consistent programming interface. A content provider behaves very much like a database — you can query it, edit its content, as well as add or delete content. However, unlike a database, a content provider can use different ways to store its data. The data can be stored in a database, in files, or even over a network.

Android ships with many useful content providers, including the following:

  • Browser — Stores data such as browser bookmarks, browser history, and so on
  • CallLog — Stores data such as missed calls, call details, and so on
  • Contacts — Stores contact details
  • MediaStore — Stores media files such as audio, video, and images
  • Settings — Stores the device’s settings and preferences

Besides the many built-in content providers, you can also create your own content providers.

To query a content provider, you specify the query string in the form of a URI, with an optional specifier for a particular row. The format of the query URI is as follows:

<standard_prefix>://<authority>/<data_path>/<id>

The various parts of the URI are as follows:

  • The standard prefix for content providers is always content://.
  • The authority specifies the name of the content provider. An example would be contacts for the built-in Contacts content provider. For third-party content providers, this could be the fully qualified name, such as com.wrox.provider or net.learn2develop.provider.
  • The data path specifies the kind of data requested. For example, if you are getting all the contacts from the Contacts content provider, then the data path would be people, and the URI would look like this: content://contacts/people.
  • The id specifies the specific record requested. For example, if you are looking for contact number 2 in the Contacts content provider, the URI would look like this: content://contacts/people/2.

Table 7-1 shows some examples of query strings.

Table 7.1: Example Query Strings

QUERY STRING DESCRIPTION
content://media/internal/images Returns a list of all the internal images on the device
content://media/external/images Returns a list of all the images stored on the external storage (e.g., SD card) on the device
content://call_log/calls Returns a list of all calls registered in the Call Log
content://browser/bookmarks Returns a list of bookmarks stored in the browser

USING A CONTENT PROVIDER

The best way to understand content providers is to actually use one. The following Try It Out shows how you can use a content provider from within your Android application.

TRY IT OUT: Using the Contacts Content Provider

codefile Provider.zip available for download at Wrox.com

1. Using Eclipse, create a new Android project and name it Provider.

2. Add the following statements in bold to the main.xml file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
 
<ListView 
    android:id="@+id/android:list"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:stackFromBottom="false"
    android:transcriptMode="normal" />
 
<TextView
    android:id="@+id/contactName"
    android:textStyle="bold"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
 
<TextView
    android:id="@+id/contactID"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
 
</LinearLayout> 

3. In the ProviderActivity.java class, code the following:

package net.learn2develop.Provider;
 
import android.app.ListActivity;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.CursorAdapter;
import android.widget.SimpleCursorAdapter;
 
public class ProviderActivity extends ListActivity  {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Uri allContacts = Uri.parse("content://contacts/people");
 
        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, null, null, null, null);
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    null, 
                    null,
                    null , 
                    null);
            c = cursorLoader.loadInBackground();            
        }
        
        String[] columns = new String[] {
            ContactsContract.Contacts.DISPLAY_NAME,
            ContactsContract.Contacts._ID};
        
        int[] views = new int[] {R.id.contactName, R.id.contactID};
 
        SimpleCursorAdapter adapter;
        
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views);
        } else {
            //---Honeycomb and later---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views,
                    CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);            
        }
        
        this.setListAdapter(adapter);   }
} 

4. Add the following statements in bold to the AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.learn2develop.Provider"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk android:minSdkVersion="14" />
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
 
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".ProviderActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest> 

5. Launch an AVD and create a few contacts in the Android Emulator. To add a contact, go to the Phone application and click the Star icon at the top (see Figure 7-1). Click the MENU button on the emulator and click the New contact menu item. You will be warned about backing up your contacts. Click the Keep Local button and enter the name, phone number, and e-mail address of a few people.

6. Press F11 to debug the application on the Android emulator. Figure 7-2 shows the activity displaying the list of contacts you just created.

How It Works

In this example, you retrieved all the contacts stored in the Contacts application and displayed them in the ListView.

First, you specified the URI for accessing the Contacts application:

Uri allContacts = Uri.parse("content://contacts/people");

Next, observe that you have a conditional check to detect the version of the device on which the application is currently running:

Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, null, null, null, null);
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    null, 
                    null,
                    null , 
                    null);
            c = cursorLoader.loadInBackground();            
        }

If the application is running on pre-Honeycomb devices (the value of the android.os.Build.VERSION.SDK_INT variable is lower than 11), you can use the managedQuery() method of the Activity class to retrieve a managed cursor. A managed cursor handles all the work of unloading itself when the application pauses and requerying itself when the application restarts. The statement

Cursor c = managedQuery(allContacts, null, null, null, null);

is equivalent to

Cursor c = getContentResolver().query(allContacts, null, null, null, null);
        //---allows the activity to manage the Cursor's
        // lifecyle based on the activity's lifecycle---
        startManagingCursor(c); 

The getContentResolver() method returns a ContentResolver object, which helps to resolve a content URI with the appropriate content provider.

However, beginning with Android API level 11 (Honeycomb and later), the managedQuery() method is deprecated (still available but not recommended for use). For Honeycomb or later devices, use the CursorLoader class:

    CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    null, 
                    null,
                    null , 
                    null);
            c = cursorLoader.loadInBackground();            

The CursorLoader class (only available beginning with Android API level 11 and later) performs the cursor query on a background thread and hence does not block the application UI.

The SimpleCursorAdapter object maps a cursor to TextViews (or ImageViews) defined in your XML file (main.xml). It maps the data (as represented by columns) to views (as represented by views):

String[] columns = new String[] {
            ContactsContract.Contacts.DISPLAY_NAME,
            ContactsContract.Contacts._ID};
        
        int[] views = new int[] {R.id.contactName, R.id.contactID};
 
        SimpleCursorAdapter adapter;
        
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views);
        } else {
            //---Honeycomb and later---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views,
                    CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);            
        }
        
        this.setListAdapter(adapter);

Like the managedQuery() method, one of the constructors for the SimpleCursorAdapter class has been deprecated. For Honeycomb or later devices, you need to use the new constructor for the SimpleCursorAdapter class with one additional argument:

    //---Honeycomb and later---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views,
                    CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);            

The flag registers the adapter to be informed when there is a change in the content provider.

Note that in order for your application to access the Contacts application, you need to have the READ_CONTACTS permission in your AndroidManifest.xml file.

Predefined Query String Constants

Besides using the query URI, you can use a list of predefined query string constants in Android to specify the URI for the different data types. For example, besides using the query content://contacts/people, you can rewrite the statement

        Uri allContacts = Uri.parse("content://contacts/people");

using one of the predefined constants in Android, as follows:

        Uri allContacts = ContactsContract.Contacts.CONTENT_URI;
image

NOTE For Android 2.0 and later, to query the base Contacts records you need to use the ContactsContract.Contacts.CONTENT_URI URI.

Some examples of predefined query string constants are as follows:

  • Browser.BOOKMARKS_URI
  • Browser.SEARCHES_URI
  • CallLog.CONTENT_URI
  • MediaStore.Images.Media.INTERNAL_CONTENT_URI
  • MediaStore.Images.Media.EXTERNAL_CONTENT_URI
  • Settings.CONTENT_URI

If you want to retrieve the first contact, specify the ID of that contact, like this:

        Uri allContacts = Uri.parse("content://contacts/people/1");

Alternatively, use the predefined constant together with the withAppendedId() method of the ContentUris class:

import android.content.ContentUris;
...
        Uri allContacts = ContentUris.withAppendedId(
            ContactsContract.Contacts.CONTENT_URI, 1);

Besides binding to a ListView, you can also print out the results using the Cursor object, as shown here:

package net.learn2develop.Provider;
 
import android.app.ListActivity;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.CursorAdapter;
import android.widget.SimpleCursorAdapter;
import android.util.Log;
public class ProviderActivity extends ListActivity  {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        Uri allContacts = ContactsContract.Contacts.CONTENT_URI;
        ...
        ...
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views);
        } else {
            //---Honeycomb and later---
            adapter = new SimpleCursorAdapter(
                    this, R.layout.main, c, columns, views,
                    CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);            
        }
        
        this.setListAdapter(adapter);
        PrintContacts(c);
    }
    
    private void PrintContacts(Cursor c)
    {
        if (c.moveToFirst()) {
            do{
            String contactID = c.getString(c.getColumnIndex(
                   ContactsContract.Contacts._ID));
            String contactDisplayName = 
                  c.getString(c.getColumnIndex(
                      ContactsContract.Contacts.DISPLAY_NAME));
                Log.v("Content Providers", contactID + ", " +
                    contactDisplayName);
            } while (c.moveToNext());
        }
    } 
}
image

NOTE If you don’t know how to view the LogCat window, refer to Appendix A for a quick tour of the Eclipse IDE.

The PrintContacts() method will print out the following in the LogCat window:

12-13 08:32:50.471: V/Content Providers(12346): 1, Wei-Meng Lee
12-13 08:32:50.471: V/Content Providers(12346): 2, Linda Chen
12-13 08:32:50.471: V/Content Providers(12346): 3, Joanna Yip

It prints out the ID and name of each contact stored in the Contacts application. In this case, you access the ContactsContract.Contacts._ID field to obtain the ID of a contact, and ContactsContract.Contacts.DISPLAY_NAME for the name of a contact. If you want to display the phone number of a contact, you need to query the content provider again, as the information is stored in another table:

    private void PrintContacts(Cursor c)
    {
        if (c.moveToFirst()) {
            do{
                String contactID = c.getString(c.getColumnIndex(
                        ContactsContract.Contacts._ID));
                String contactDisplayName = 
                        c.getString(c.getColumnIndex(
                                ContactsContract.Contacts.DISPLAY_NAME));
                Log.v("Content Providers", contactID + ", " +
                        contactDisplayName);
 
                //---get phone number---
                int hasPhone =
                        c.getInt(c.getColumnIndex(
                                ContactsContract.Contacts.HAS_PHONE_NUMBER));
                if (hasPhone == 1) {
                    Cursor phoneCursor = 
                        getContentResolver().query(
                            ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
                            ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " +
                            contactID, null, null);
                    while (phoneCursor.moveToNext()) {
                        Log.v("Content Providers",
                            phoneCursor.getString(
                                phoneCursor.getColumnIndex(
                                    ContactsContract.CommonDataKinds.Phone.NUMBER)));
                    }
                    phoneCursor.close();
                }
 
            } while (c.moveToNext());
        }
    }
image

NOTE To access the phone number of a contact, you need to query against the URI stored in ContactsContract.CommonDataKinds.Phone.CONTENT_URI.

In the preceding code snippet, you first check whether a contact has a phone number using the ContactsContract.Contacts.HAS_PHONE_NUMBER field. If the contact has at least a phone number, you then query the content provider again based on the ID of the contact. Once the phone number(s) are retrieved, you then iterate through them and print out the numbers. You should see something like this:

12-13 08:59:31.881: V/Content Providers(13351): 1, Wei-Meng Lee
12-13 08:59:32.311: V/Content Providers(13351): +651234567
12-13 08:59:32.321: V/Content Providers(13351): 2, Linda Chen
12-13 08:59:32.511: V/Content Providers(13351): +1 876-543-21
12-13 08:59:32.545: V/Content Providers(13351): 3, Joanna Yip
12-13 08:59:32.641: V/Content Providers(13351): +239 846 5522

Projections

The second parameter of the managedQuery() method (third parameter for the CursorLoader class) controls how many columns are returned by the query; this parameter is known as the projection. Earlier, you specified null:

        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, null, null, null, null);
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    null, 
                    null,
                    null , 
                    null);
            c = cursorLoader.loadInBackground();            
        }

You can specify the exact columns to return by creating an array containing the name of the column to return, like this:

        String[] projection = new String[]
                {ContactsContract.Contacts._ID,
                 ContactsContract.Contacts.DISPLAY_NAME,
                 ContactsContract.Contacts.HAS_PHONE_NUMBER};
        
        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, projection, null, null, null);
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    projection, 
                    null,
                    null , 
                    null);
            c = cursorLoader.loadInBackground();            
        }

In the above case, the _ID, DISPLAY_NAME, and HAS_PHONE_NUMBER fields will be retrieved.

Filtering

The third and fourth parameters of the managedQuery() method (fourth and fifth parameters for the CursorLoader class) enable you to specify a SQL WHERE clause to filter the result of the query. For example, the following statement retrieves only the people whose name ends with “Lee”:

        Cursor c;
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, projection, 
                    ContactsContract.Contacts.DISPLAY_NAME + " LIKE '%Lee'", null, null);
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    projection, 
                    ContactsContract.Contacts.DISPLAY_NAME + " LIKE '%Lee'",
                    null , 
                    null);
            c = cursorLoader.loadInBackground();            
        }

Here, the third parameter (for the managedQuery() method, and the fourth parameter for the CursorLoader constructor) contains a SQL statement containing the name to search for (“Lee”). You can also put the search string into the next argument of the method/constructor, like this:

        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, projection, 
                    ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
                    new String[] {"%Lee"}, null);
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    projection, 
                    ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
                    new String[] {"%Lee"}, 
                    null);
            c = cursorLoader.loadInBackground();            
        }

Sorting

The last parameter of the managedQuery() method (and constructor for the CursorLoader class) enables you to specify a SQL ORDER BY clause to sort the result of the query. For example, the following statement sorts the contact names in ascending order:

        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allContacts, projection, 
                    ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
                    new String[] {"%Lee"}, 
                    ContactsContract.Contacts.DISPLAY_NAME + " ASC");
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allContacts, 
                    projection, 
                    ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?",
                    new String[] {"%Lee"}, 
                    ContactsContract.Contacts.DISPLAY_NAME + " ASC");
            c = cursorLoader.loadInBackground();            
        }

CREATING YOUR OWN CONTENT PROVIDERS

Creating your own content provider in Android is relatively simple. All you need to do is extend the abstract ContentProvider class and override the various methods defined within it.

In this section, you will learn how to create a simple content provider that stores a list of books. For ease of illustration, the content provider stores the books in a database table containing three fields, as shown in Figure 7-3.

The following Try It Out shows you the steps.

TRY IT OUT: Creating Your Own Content Provider

codefile ContentProviders.zip available for download at Wrox.com

1. Using Eclipse, create a new Android project and name it ContentProviders.

2. In the src folder of the project, add a new Java class file and name it BooksProvider.

3. Populate the BooksProvider.java file as follows:

package net.learn2develop.ContentProviders;
 
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
 
public class BooksProvider extends ContentProvider {
    static final String PROVIDER_NAME =
        "net.learn2develop.provider.Books";
 
    static final Uri CONTENT_URI =
        Uri.parse("content://"+ PROVIDER_NAME + "/books");
 
    static final String _ID = "_id";
    static final String TITLE = "title";
    static final String ISBN = "isbn";
 
    static final int BOOKS = 1;
    static final int BOOK_ID = 2;
 
    private static final UriMatcher uriMatcher;
    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(PROVIDER_NAME, "books", BOOKS);
        uriMatcher.addURI(PROVIDER_NAME, "books/#", BOOK_ID);
    }
 
    //---for database use---
    SQLiteDatabase booksDB;
    static final String DATABASE_NAME = "Books";
    static final String DATABASE_TABLE = "titles";
    static final int DATABASE_VERSION = 1;
    static final String DATABASE_CREATE =
        "create table " + DATABASE_TABLE +
        " (_id integer primary key autoincrement, "
        + "title text not null, isbn text not null);";
 
    private static class DatabaseHelper extends SQLiteOpenHelper
    {
        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
 
        @Override
        public void onCreate(SQLiteDatabase db)
        {
            db.execSQL(DATABASE_CREATE);
        }
 
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion,
                int newVersion) {
            Log.w("Content provider database",
                    "Upgrading database from version " +
                            oldVersion + " to " + newVersion +
                    ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS titles");
            onCreate(db);
        }
    }
    
    @Override
    public int delete(Uri arg0, String arg1, String[] arg2) {
        // arg0 = uri
        // arg1 = selection
        // arg2 = selectionArgs
        int count=0;
        switch (uriMatcher.match(arg0)){
        case BOOKS:
            count = booksDB.delete(
                    DATABASE_TABLE,
                    arg1,
                    arg2);
            break;
        case BOOK_ID:
            String id = arg0.getPathSegments().get(1);
            count = booksDB.delete(
                    DATABASE_TABLE,
                    _ID + " = " + id +
                    (!TextUtils.isEmpty(arg1) ? " AND (" +
                            arg1 + ')' : ""),
                            arg2);
            break;
        default: throw new IllegalArgumentException("Unknown URI " + arg0);
        }
        getContext().getContentResolver().notifyChange(arg0, null);
        return count;
    }
 
    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
        //---get all books---
        case BOOKS:
            return "vnd.android.cursor.dir/vnd.learn2develop.books ";
            
        //---get a particular book---
        case BOOK_ID:
            return "vnd.android.cursor.item/vnd.learn2develop.books ";
            
        default:
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }
 
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        //---add a new book---
        long rowID = booksDB.insert(
                DATABASE_TABLE,
                "",
                values);
 
        //---if added successfully---
        if (rowID>0)
        {
            Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
            getContext().getContentResolver().notifyChange(_uri, null);
            return _uri;
        }
        throw new SQLException("Failed to insert row into " + uri);
    }
 
    @Override
    public boolean onCreate() {
        Context context = getContext();
        DatabaseHelper dbHelper = new DatabaseHelper(context);
        booksDB = dbHelper.getWritableDatabase();
        return (booksDB == null)? false:true;
    }
 
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
        sqlBuilder.setTables(DATABASE_TABLE);
 
        if (uriMatcher.match(uri) == BOOK_ID)
            //---if getting a particular book---
            sqlBuilder.appendWhere(
                    _ID + " = " + uri.getPathSegments().get(1));
 
        if (sortOrder==null || sortOrder=="")
            sortOrder = TITLE;
 
        Cursor c = sqlBuilder.query(
            booksDB,
            projection,
            selection,
            selectionArgs,
            null,
            null,
            sortOrder);
 
        //---register to watch a content URI for changes---
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }
 
    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        int count = 0;
        switch (uriMatcher.match(uri)){
        case BOOKS:
            count = booksDB.update(
                    DATABASE_TABLE,
                    values,
                    selection,
                    selectionArgs);
            break;
        case BOOK_ID:
            count = booksDB.update(
                    DATABASE_TABLE,
                    values,
                    _ID + " = " + uri.getPathSegments().get(1) +
                    (!TextUtils.isEmpty(selection) ? " AND (" +
                            selection + ')' : ""),
                            selectionArgs);
            break;
        default: throw new IllegalArgumentException("Unknown URI " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
}

4. Add the following statements in bold to the AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.learn2develop.ContentProviders"
    android:versionCode="1"
    android:versionName="1.0" >
 
    <uses-sdk android:minSdkVersion="14" />
 
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".ContentProvidersActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <provider android:name="BooksProvider"
            android:authorities="net.learn2develop.provider.Books">            
        </provider>            
    </application>
 
</manifest>

How It Works

In this example, you first created a class named BooksProvider that extends the ContentProvider base class. The various methods to override in this class are as follows:

  • getType() — Returns the MIME type of the data at the given URI
  • onCreate() — Called when the provider is started
  • query() — Receives a request from a client. The result is returned as a Cursor object.
  • insert() — Inserts a new record into the content provider
  • delete() — Deletes an existing record from the content provider
  • update() — Updates an existing record from the content provider

Within your content provider, you are free to choose how you want to store your data — a traditional file system, XML, a database, or even through web services. For this example, you used the SQLite database approach discussed in the previous chapter.

You then defined the following constants within the BooksProvider class:

    static final String PROVIDER_NAME =
        "net.learn2develop.provider.Books";
 
    static final Uri CONTENT_URI =
        Uri.parse("content://"+ PROVIDER_NAME + "/books");
 
    static final String _ID = "_id";
    static final String TITLE = "title";
    static final String ISBN = "isbn";
 
    static final int BOOKS = 1;
    static final int BOOK_ID = 2;
 
    private static final UriMatcher uriMatcher;
    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(PROVIDER_NAME, "books", BOOKS);
        uriMatcher.addURI(PROVIDER_NAME, "books/#", BOOK_ID);
    }
 
    //---for database use---
    SQLiteDatabase booksDB;
    static final String DATABASE_NAME = "Books";
    static final String DATABASE_TABLE = "titles";
    static final int DATABASE_VERSION = 1;
    static final String DATABASE_CREATE =
        "create table " + DATABASE_TABLE +
        " (_id integer primary key autoincrement, "
        + "title text not null, isbn text not null);";

Observe in the preceding code that you used an UriMatcher object to parse the content URI that is passed to the content provider through a ContentResolver. For example, the following content URI represents a request for all books in the content provider:

content://net.learn2develop.provider.Books/books

The following represents a request for a particular book with _id 5:

content://net.learn2develop.provider.Books/books/5

Your content provider uses a SQLite database to store the books. Note that you used the SQLiteOpenHelper helper class to help manage your database:

    private static class DatabaseHelper extends SQLiteOpenHelper
    {
        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
 
        @Override
        public void onCreate(SQLiteDatabase db)
        {
            db.execSQL(DATABASE_CREATE);
        }
 
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion,
                int newVersion) {
            Log.w("Content provider database",
                    "Upgrading database from version " +
                            oldVersion + " to " + newVersion +
                    ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS titles");
            onCreate(db);
        }
    } 

Next, you override the getType() method to uniquely describe the data type for your content provider. Using the UriMatcher object, you returned vnd.android.cursor.item/vnd.learn2develop.books for a single book, and vnd.android.cursor.dir/vnd.learn2develop.books for multiple books:

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
        //---get all books---
        case BOOKS:
            return "vnd.android.cursor.dir/vnd.learn2develop.books ";
            
        //---get a particular book---
        case BOOK_ID:
            return "vnd.android.cursor.item/vnd.learn2develop.books ";
            
        default:
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }

Next, you overrode the onCreate() method to open a connection to the database when the content provider is started:

    @Override
    public boolean onCreate() {
        Context context = getContext();
        DatabaseHelper dbHelper = new DatabaseHelper(context);
        booksDB = dbHelper.getWritableDatabase();
        return (booksDB == null)? false:true;
    }

You overrode the query() method to allow clients to query for books:

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
        sqlBuilder.setTables(DATABASE_TABLE);
 
        if (uriMatcher.match(uri) == BOOK_ID)
            //---if getting a particular book---
            sqlBuilder.appendWhere(
                    _ID + " = " + uri.getPathSegments().get(1));
 
        if (sortOrder==null || sortOrder=="")
            sortOrder = TITLE;
 
        Cursor c = sqlBuilder.query(
            booksDB,
            projection,
            selection,
            selectionArgs,
            null,
            null,
            sortOrder);
 
        //---register to watch a content URI for changes---
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

By default, the result of the query is sorted using the title field. The resulting query is returned as a Cursor object.

To allow a new book to be inserted into the content provider, you override the insert() method:

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        //---add a new book---
        long rowID = booksDB.insert(
                DATABASE_TABLE,
                "",
                values);
 
        //---if added successfully---
        if (rowID>0)
        {
            Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
            getContext().getContentResolver().notifyChange(_uri, null);
            return _uri;
        }
        throw new SQLException("Failed to insert row into " + uri);
    }

Once the record is inserted successfully, you call the notifyChange() method of the ContentResolver. This notifies registered observers that a row was updated.

To delete a book, you override the delete() method:

    @Override
    public int delete(Uri arg0, String arg1, String[] arg2) {
        // arg0 = uri
        // arg1 = selection
        // arg2 = selectionArgs
        int count=0;
        switch (uriMatcher.match(arg0)){
        case BOOKS:
            count = booksDB.delete(
                    DATABASE_TABLE,
                    arg1,
                    arg2);
            break;
        case BOOK_ID:
            String id = arg0.getPathSegments().get(1);
            count = booksDB.delete(
                    DATABASE_TABLE,
                    _ID + " = " + id +
                    (!TextUtils.isEmpty(arg1) ? " AND (" +
                            arg1 + ')' : ""),
                            arg2);
            break;
        default: throw new IllegalArgumentException("Unknown URI " + arg0);
        }
        getContext().getContentResolver().notifyChange(arg0, null);
        return count;
    }

Likewise, call the notifyChange() method of the ContentResolver after the deletion. This will notify registered observers that a row was deleted.

Finally, to update a book, you override the update() method:

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        int count = 0;
        switch (uriMatcher.match(uri)){
        case BOOKS:
            count = booksDB.update(
                    DATABASE_TABLE,
                    values,
                    selection,
                    selectionArgs);
            break;
        case BOOK_ID:
            count = booksDB.update(
                    DATABASE_TABLE,
                    values,
                    _ID + " = " + uri.getPathSegments().get(1) +
                    (!TextUtils.isEmpty(selection) ? " AND (" +
                            selection + ')' : ""),
                            selectionArgs);
            break;
        default: throw new IllegalArgumentException("Unknown URI " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

As with the insert() and delete() methods, you called the notifyChange() method of the ContentResolver after the update. This notifies registered observers that a row was updated.

Finally, to register your content provider with Android, modify the AndroidManifest.xml file by adding the <provider> element.

USING THE CONTENT PROVIDER

Now that you have built your new content provider, you can test it from within your Android application. The following Try It Out demonstrates how to do that.

TRY IT OUT: Using the Newly Created Content Provider

1. Using the same project created in the previous section, add the following statements in bold to the main.xml file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
 
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="ISBN" />
 
<EditText
    android:id="@+id/txtISBN"
    android:layout_height="wrap_content"
    android:layout_width="fill_parent" />
 
<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="Title" />
 
<EditText
    android:id="@+id/txtTitle" 
    android:layout_height="wrap_content"
    android:layout_width="fill_parent" />
 
<Button
    android:text="Add title"
    android:id="@+id/btnAdd"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:onClick="onClickAddTitle" />
 
<Button
    android:text="Retrieve titles"
    android:id="@+id/btnRetrieve"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:onClick="onClickRetrieveTitles"  />
 
</LinearLayout>

2. In the ContentProvidersActivity.java file, add the following statements in bold:

package net.learn2develop.ContentProviders;
 
import android.app.Activity;
import android.content.ContentValues;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
 
public class ContentProvidersActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
 
    public void onClickAddTitle(View view) {
        //---add a book---
        ContentValues values = new ContentValues();
        values.put(BooksProvider.TITLE, ((EditText)
                findViewById(R.id.txtTitle)).getText().toString());
        values.put(BooksProvider.ISBN, ((EditText)
                findViewById(R.id.txtISBN)).getText().toString());
        Uri uri = getContentResolver().insert(
                BooksProvider.CONTENT_URI, values);
        Toast.makeText(getBaseContext(),uri.toString(),
                Toast.LENGTH_LONG).show();
    }
 
    public void onClickRetrieveTitles(View view) {
        //---retrieve the titles---
        Uri allTitles = Uri.parse(
                "content://net.learn2develop.provider.Books/books");
        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allTitles, null, null, null,
                    "title desc");
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allTitles, null, null, null,
                    "title desc");
            c = cursorLoader.loadInBackground();            
        }
        if (c.moveToFirst()) {
            do{
                Toast.makeText(this, 
                    c.getString(c.getColumnIndex(
                        BooksProvider._ID)) + ", " +
                    c.getString(c.getColumnIndex(
                        BooksProvider.TITLE)) + ", " +
                    c.getString(c.getColumnIndex(
                        BooksProvider.ISBN)),
                    Toast.LENGTH_SHORT).show();
            } while (c.moveToNext());
        }
    }
 
}

3. Press F11 to debug the application on the Android emulator.

4. Enter an ISBN and title for a book and click the Add title button. Figure 7-4 shows the Toast class displaying the URI of the book added to the content provider. To retrieve all the titles stored in the content provider, click the Retrieve titles button and observe the values displayed using the Toast class.

How It Works

First, you modified the activity so that users can enter a book’s ISBN and title to add to the content provider that you have just created.

To add a book to the content provider, you create a new ContentValues object and then populate it with the various information about a book:

//---add a book---
        ContentValues values = new ContentValues();
        values.put(BooksProvider.TITLE, ((EditText)
                findViewById(R.id.txtTitle)).getText().toString());
        values.put(BooksProvider.ISBN, ((EditText)
                findViewById(R.id.txtISBN)).getText().toString());
        Uri uri = getContentResolver().insert(
                BooksProvider.CONTENT_URI, values);

Notice that because your content provider is in the same package, you can use the BooksProvider.TITLE and the BooksProvider.ISBN constants to refer to the "title" and "isbn" fields, respectively. If you were accessing this content provider from another package, then you would not be able to use these constants. In that case, you need to specify the field name directly, like this:

ContentValues values = new ContentValues();
        values.put("title", ((EditText)
            findViewById(R.id.txtTitle)).getText().toString());
        values.put("isbn", ((EditText)
            findViewById(R.id.txtISBN)).getText().toString());
        Uri uri = getContentResolver().insert(
                Uri.parse(
                    "content://net.learn2develop.provider.Books/books"),
                    values);

Also note that for external packages, you need to refer to the content URI using the fully qualified content URI:

        Uri.parse(
                    "content://net.learn2develop.provider.Books/books"),

To retrieve all the titles in the content provider, you used the following code snippets:

//---retrieve the titles---
        Uri allTitles = Uri.parse(
                "content://net.learn2develop.provider.Books/books");
        Cursor c; 
        if (android.os.Build.VERSION.SDK_INT <11) {
            //---before Honeycomb---
            c = managedQuery(allTitles, null, null, null,
                    "title desc");
        } else {
            //---Honeycomb and later---
            CursorLoader cursorLoader = new CursorLoader(
                    this, 
                    allTitles, null, null, null,
                    "title desc");
            c = cursorLoader.loadInBackground();            
        }
        if (c.moveToFirst()) {
            do{
                Toast.makeText(this, 
                    c.getString(c.getColumnIndex(
                        BooksProvider._ID)) + ", " +
                    c.getString(c.getColumnIndex(
                        BooksProvider.TITLE)) + ", " +
                    c.getString(c.getColumnIndex(
                        BooksProvider.ISBN)),
                    Toast.LENGTH_SHORT).show();
            } while (c.moveToNext());
        }

The preceding query will return the result sorted in descending order based on the title field.

If you want to update a book’s detail, call the update() method with the content URI, indicating the book’s ID:

ContentValues editedValues = new ContentValues();
        editedValues.put(BooksProvider.TITLE, "Android Tips and Tricks");
        getContentResolver().update(
            Uri.parse(
                "content://net.learn2develop.provider.Books/books/2"),
                editedValues,
                null,
                null);

To delete a book, use the delete() method with the content URI, indicating the book’s ID:

        //---delete a title---
        getContentResolver().delete(
                Uri.parse("content://net.learn2develop.provider.Books/books/2"),
                null, null);

To delete all books, simply omit the book’s ID in your content URI:

        //---delete all titles---
        getContentResolver().delete(
                Uri.parse("content://net.learn2develop.provider.Books/books"),
                null, null);

SUMMARY

In this chapter, you learned what content providers are and how to use some of the built-in content providers in Android. In particular, you have seen how to use the Contacts content provider. Google’s decision to provide content providers enables applications to share data through a standard set of programming interfaces. In addition to the built-in content providers, you can also create your own custom content provider to share data with other packages.

EXERCISES

1. Write the query to retrieve all contacts from the Contacts application that contain the word “jack.”

2. Name the methods that you need to override in your own implementation of a content provider.

3. How do you register a content provider in your AndroidManifest.xml file?

Answers to the exercises can be found in Appendix C.

• WHAT YOU LEARNED IN THIS CHAPTER

TOPIC KEY CONCEPTS
Retrieving a managed cursor Use the managedQuery() method (for pre-Honeycomb devices) or use the CursorLoader class (for Honeycomb or later devices).
Two ways to specify a query for a content provider Use either a query URI or a predefined query string constant.
Retrieving the value of a column in a content provider Use the getColumnIndex() method.
Query URI for accessing a contact’s name ContactsContract.Contacts.CONTENT_URI
Query URI for accessing a contact’s phone number ContactsContract.CommonDataKinds.Phone.CONTENT_URI
Creating your own content provider Create a class and extend the ContentProvider class.
..................Content has been hidden....................

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