Content Provider in Android

1 comment

Introduction

Storing your data in a database is one good way to persist your data, but there's a caveat in Android—databases created in Android are visible only to the application that created them. That is to say, a SQLite database created on Android by one application is usable only by that application, not by other applications.
So, if you need to share data between applications, you need to use the content provider model as recommended in Android. This article presents the basics of content providers and how you can implement one.

Using a Content Provider

In Android, a content provider is a specialized type of data store that exposes standardized ways to retrieve and manipulate the stored data. Android ships with several useful content providers, as shown in Table below.

content_providers_android_01

To query a content provider, you provide a query string in the form of a URI, with an optional specifier for a particular row, using the following syntax:


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

For example, to retrieve all the bookmarks stored by your web browsers (in Android), you would use the following content URI:

content://browser/bookmarks

Similarly, to retrieve all the contacts stored by the Contacts application, the URI would look like this:

content://contacts/people
To retrieve a particular contact, you can specify the URI with a specific ID:

content://contacts/people/3
Of course, you can access all this information programmatically as well. The rest of this article walks you through the process.

Build a Custom Content Provider

To get started, create a new Android project in Eclipse. Name the project "CPExamples" as shown in Figure 1.

Figure 1. New Android Project: This dialog lets you create a new Android project in Eclipse.
After Eclipse creates the project, add the code below to the CPExampleActivity class.


package net.learn2develop.CPExamples;

import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CallLog.Calls;
import android.util.Log;
public class CPExampleActivity extends Activity {
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      Uri allCalls = Uri.parse("content://call_log/calls");
      Cursor c = managedQuery(allCalls, null, null, null, null);      
      if (c.moveToFirst()) {
         do{            
            String callType = "";
            switch (Integer.parseInt(c.getString(
              c.getColumnIndex(Calls.TYPE))))
            {
                  case 1: callType = "Incoming";
                     break;
                  case 2: callType = "Outgoing";
                     break;
                  case 3: callType = "Missed";
            }
            Log.v("Content Providers", 
                  c.getString(c.getColumnIndex(Calls._ID)) + ", " +
                  c.getString(c.getColumnIndex(Calls.NUMBER)) + ", " +
                  callType) ;
         } while (c.moveToNext());
      } 
   }
}


The code above accesses the call log of your Android device and prints out all the calls that you have made. The Activity class's managedQuery() method retrieves a managed cursor, which handles all the work of unloading itself when the application pauses, and re-querying itself when the application restarts. Using the Cursor object, you can iteratively access the results returned by the query.

For this program to work, you would also need to add in the READ_CONTACTS permission in the AndroidManifest.xml file:


<?xml version="1.0" encoding="utf-8"?>

   <manifest xmlns:android=
     "http://schemas.android.com/apk/res/android"
     package="net.learn2develop.CPExamples"
     android:versionCode="1"
     android:versionName="1.0.0">
     <application android:icon="@drawable/icon" 
       android:label="@string/app_name">
       <activity android:name=".CPExampleActivity"
         android:label="@string/app_name">
         <intent-filter>
           <action android:name="android.intent.action.MAIN" />
           <category android:name=
             "android.intent.category.LAUNCHER" />
         </intent-filter>
       </activity>
     </application>
     <uses-permission 
       android:name="android.permission.READ_CONTACTS">
     </uses-permission>
   </manifest>

When you run the program, you should see something like this in the LogCat window—a list of all the calls made on the Android handset:

   02-23 01:41:15.130: VERBOSE/Content
      Providers(774): 1, 654565677, Outgoing
   02-23 01:41:15.130: VERBOSE/Content
      Providers(774): 2, 658373543, Outgoing
   02-23 01:41:15.140: VERBOSE/Content
      Providers(774): 3, 653233350, Missed
   02-23 01:41:15.152: VERBOSE/Content
      Providers(774): 4, 659888988, Incoming

Note that the managedQuery() method accepts five arguments:

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

The second argument to the managedQuery() method, known as "projections," controls how many columns the query returns. You can specify exactly which columns the method should return by creating an array containing the name of the desired columns. For example:

String[] projection = new String[] {
      Calls._ID, Calls.NUMBER, Calls.TYPE};                
   Cursor c = managedQuery(allCalls, projection, 
      null, null, null);

The third and fourth parameters, known as "Filtering," let you specify a SQL WHERE clause to filter the query results. You can specify a filtering rule like this:

Cursor c = managedQuery(allCalls, projection, 
      "Calls.NUMBER LIKE '65%'", //---retrieve numbers beginning with 65---
      null, null);

The fifth and last parameter lets you specify a SQL ORDER BY clause to sort the query results. This parameter is also known as "Sorting." You can specify a sorting rule like this:


Cursor c = managedQuery(allCalls, projection, 
      "Calls.NUMBER LIKE '5%'", 
      null, 
      "Calls.TYPE DESC"); //---sort result by call TYPE descending--- 

Creating Your Own Content Providers

To create your own content provider in Android, all you need to do is to extend the abstract ContentProvider class and override the various methods defined within it.

Here's a simple content provider example that stores a list of books. For simplicity, the content provider stores the books in a table containing three fields as shown in Figure 2.

content_providers_android_03
Figure 2. Content Provider Structure: The figures shows the structure of the content provider that you will be building.

Extend the ContentProvider class, import the various namespaces and implement the methods as shown below.

package net.learn2develop.CPExamples;
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 
{
   @Override
   public int delete(Uri arg0, String arg1, String[] arg2) {
      return 0;
   }
   @Override
   public String getType(Uri uri) {
      return null;
   }
   @Override
   public Uri insert(Uri uri, ContentValues values) {
      return null;
   }
   @Override
   public boolean onCreate() {
      return false;
   }
   @Override
   public Cursor query(Uri uri, String[] projection, String selection,
         String[] selectionArgs, String sortOrder) {
      return null;
   }
   @Override
   public int update(Uri uri, ContentValues values, String selection,
         String[] selectionArgs) {
      return 0;
   }
}

The methods you need to implement are:
  • getType(): Returns the MIME type of the data at the given URI.
  • onCreate(): Called when the provider is being 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 may choose to store your data however you like: traditional file system, XML, database, or even through web services.

Define the following constants within the BooksProvider class:

public class BooksProvider extends ContentProvider 
   {
      public static final String PROVIDER_NAME = 
         "net.learn2develop.provider.Books";
      public static final Uri CONTENT_URI = 
         Uri.parse("content://"+ PROVIDER_NAME + "/books");
      public static final String _ID = "_id";
      public static final String TITLE = "title";
      public static final String ISBN = "isbn";
      private static final int BOOKS = 1;
      private 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);      
      }
      //...
   }

Observe from the code above that you use a UriMatcher object to parse the content URI that is passed to the content provider through a Content ResolverFor example, the following content URI represents a request for all books in the content provider:

   content://net.learn2develop.provider.Books/books
In contrast, the following represents a request for a particular book with _id=5:

   content://net.learn2develop.provider.MailingList/books/5
Define the code below, so the content provider will use a SQLite database to store the books in the content provider. Note that you use the SQLiteOpenHelper helper class to help manage your database.

public class BooksProvider extends ContentProvider 
{
   public static final String PROVIDER_NAME = 
      "net.learn2develop.provider.Books";
   public static final Uri CONTENT_URI = 
      Uri.parse("content://"+ PROVIDER_NAME + "/books");
   public static final String _ID = "_id";
   public static final String TITLE = "title";
   public static final String ISBN = "isbn";
   private static final int BOOKS = 1;
   private 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---
   private SQLiteDatabase booksDB;
   private static final String DATABASE_NAME = "Books";
   private static final String DATABASE_TABLE = "titles";
   private static final int DATABASE_VERSION = 1;
   private 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);
      }
   }   
   //...
   //...
}

You'll need to override the getType() method so it uniquely describes the data type for your content provider. Using the UriMatcher object, you will return "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, override 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;
   }

To allow clients to query for books, override the query() method:


@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 query result is sorted using the title field. The resulting query is returned as a Cursor object.

To allow new books to be inserted into the content provider, 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);
   }

After inserting the record successfully, call the Content Resolver's notifyChange() method to notify registered observers that a row was updated.

To delete a book, 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;      
   }

You also need to call the Content Resolver's notifyChange() method after the deletion to notify registered observers that a row was deleted.

Finally, to update a book, 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;
   }

Again, call notifyChange() to let registered observers know that a row was updated.

Registering the Content Provider

To register your content provider with Android, modify the AndroidManifest.xml file by adding the element:

<?xml version="1.0" encoding="utf-8"?>
   <manifest xmlns:android=
     "http://schemas.android.com/apk/res/android"
     package="net.learn2develop.CPExamples"
     android:versionCode="1"
     android:versionName="1.0.0">
     <application android:icon="@drawable/icon" 
       android:label="@string/app_name">
       <activity android:name=".CPExampleActivity"
         android:label="@string/app_name">
         <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" />        
     </application>
     <uses-permission 
       android:name="android.permission.READ_CONTACTS">
     </uses-permission>
   </manifest>

Using the Books Content Provider

With the new content provider completed, you can test it. Start by adding two books into the content provider in the CPExampleActivity.java file:

//---add a book---
   ContentValues values = new ContentValues();
   values.put(BooksProvider.TITLE, "C# 2008 Programmer's Reference");
   values.put(BooksProvider.ISBN, "0470285818");        
   Uri uri = getContentResolver().insert(
      BooksProvider.CONTENT_URI, values);
   //---add another book---
   values.clear();
   values.put(BooksProvider.TITLE, "Programming Sudoku");
   values.put(BooksProvider.ISBN, "1590596625");        
   uri = getContentResolver().insert(
      BooksProvider.CONTENT_URI, 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 prove that the two books were added correctly, create a query and then print out each book's ID, title and ISBN using the Toast class:


Uri allTitles = Uri.parse(
      "content://net.learn2develop.provider.Books/books");
   Cursor c = managedQuery(allTitles, null, null, null, "title desc");
   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_LONG).show();               
      } while (c.moveToNext());
   }

The preceding query will return results by title sorted in descending order.

To update a book's details, call the update() method using a content URI that indicates the book's ID:


ContentValues editedValues = new ContentValues();
   editedValues.put(BooksProvider.TITLE, 
      "Programming Sudoku (Apress)");
   getContentResolver().update(
      Uri.parse(
         "content://net.learn2develop.provider.Books/books/2"), 
      editedValues, 
      null, 
      null);

Use a similar content URI to delete a book:

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:


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

That's it, really. You've seen how to use the built-in content providers in Android, specifically, the CallLog content provider. The Content provider design represents a great decision by Google because it allows applications to share data through a standard set of programming interfaces. And it's extensible: You can create your own custom content provider to share your data with other packages that works just like the built-in providers.


package net.learn2develop.CPExamples;
   import android.app.Activity;
   import android.database.Cursor;
   import android.net.Uri;
   import android.os.Bundle;
   import android.provider.CallLog.Calls;
   import android.util.Log;
   public class CPExampleActivity extends Activity {
      /** Called when the activity is first created. */
      @Override
      public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
         Uri allCalls = Uri.parse("content://call_log/calls");
         Cursor c = managedQuery(allCalls, null, null, null, null);      
         if (c.moveToFirst()) {
            do{            
               String callType = "";
               switch (Integer.parseInt(c.getString(
                 c.getColumnIndex(Calls.TYPE))))
               {
                     case 1: callType = "Incoming";
                        break;
                     case 2: callType = "Outgoing";
                        break;
                     case 3: callType = "Missed";
               }
               Log.v("Content Providers", 
                     c.getString(c.getColumnIndex(Calls._ID)) + ", " +
                     c.getString(c.getColumnIndex(Calls.NUMBER)) + ", " +
                     callType) ;
            } while (c.moveToNext());
         } 
      }
   }

1 comment:

  1. this content i find since 4 days but totally not success thanks for sharing this.
    php training in ahmedabad

    ReplyDelete