Wednesday 23 May 2012

A complete Contentprovider



MyContentProvider
Much have been written about contentproviders and there are various tutorials and examples available. I walk through many of those tutorials during my learning of custom contentprovider. However none of those tutorial gave me complete and clean implementation details.
Aim of this post is to show you a complete [all necessary function implementation] and clean [well organized code for better understading] custom contentprovider.

A contentprovider is primarily designed to use for data sharing between different application. A contentprovider provide a transparent interface for structured data storage. A contentProvider  could be implemented with many different backends like SQLite, file storage or network storage. This tutorial addresses the implementation details of contentProvider implemented with SQLite database.
             

 
Lets start with an example. We have a Category class e.g
public class Category{ 
  String name;
 String status; 
}
We want to insert and get these fields in contentprovider. For this will create a content discriptor for our content provider. Lets say it Mycontentdiscriptor:


public class MyContentDescriptor {

    public static final String AUTHORITY = "sohail.aziz.mycontentprovider";
    private static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
    public static final UriMatcher URI_MATCHER = buildUriMatcher();

    private static UriMatcher buildUriMatcher() {

        // TODO Auto-generated method stub

        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

        // have to add tables uri here

        final String authority = AUTHORITY;

        //adding category Uris

        matcher.addURI(authority, Categories.CAT_PATH, Categories.CAT_PATH_TOKEN);

        matcher.addURI(authority, Categories.CAT_PATH_FOR_ID,Categories.CAT_PATH_FOR_ID_TOKEN);
     
        return matcher;

    }
    public static class Categories {

        // an identifying name for entity

        public static final String TABLE_NAME = "categories/"; 
        // the toke value are used to register path in matcher (see above)
        public static final String CAT_PATH = "categories";
        public static final int CAT_PATH_TOKEN = 100;

        public static final String CAT_PATH_FOR_ID = "categories/#";
        public static final int CAT_PATH_FOR_ID_TOKEN = 200;

        public static final Uri CONTENT_URI = BASE_URI.buildUpon()
                .appendPath(CAT_PATH).build();

        public static class Cols {
            public static final String cat_id = BaseColumns._ID;
            public static final String key_2_catname="name";
            public static final String key_3_catstatus="status";

        }
    }
}

Lets discuss the above code.
Authority: A content provider must have an Authority. Authority string must be included in manifext.xml file in order to use contentprovider. You can define it as you like.
UriMatcher : A uri matcher is used to match the URI's. Uri could be of a table or speceific row (record). For example in our case category table uri is:

content://sohail.aziz.mycontentprovider/categories


while uri for particular record in categories table could be: 


  content://sohail.aziz.mycontentprovider/categories/1



we defined two constants for these two different URI types in order to differentiate while querying.
Cols class is defining the fields of our Category object. First field of every table should be _ID. As its expected while binding to listView.

So far we were defining our content, lets create actual SQLite database to be used as backend with our contentprovider:

public class MyDatabase extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "mydatabase.db";
    private static final int DATABASE_VERSION = 1;

    // custom constructor
    public MyDatabase(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
        // TODO Auto-generated constructor stub
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // TODO Auto-generated method stub

        // creating tables categories
        db.execSQL("CREATE TABLE " + MyContentDescriptor.Categories.TABLE_NAME+ " ( "+
                 MyContentDescriptor.Categories.Cols.cat_id+ " INTEGER PRIMARY KEY AUTOINCREMENT,"+
                 MyContentDescriptor.Categories.Cols.key_2_catname    + " TEXT NOT NULL," +
                 MyContentDescriptor.Categories.Cols.key_3_catstatus + " TEXT," +
                "UNIQUE (" + 
                MyContentDescriptor.Categories.Cols.cat_id + 
            ") ON CONFLICT REPLACE)"
                );

    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO Auto-generated method stub
         if(oldVersion < newVersion){
                db.execSQL("DROP TABLE IF EXISTS " + MyContentDescriptor.Categories.TABLE_NAME);

            }
    }
}

Here we defined out database name and write sql query for the creation of Categories table. (its not created yet).

Now come to the ContentProvider: We will use both of the above classes in our content provider. You can put all this code in Contentprovider class but its better to keep these separate for organization and readability. Lets create our contentprovider e.g MyContentprovider:


public class MyContentProvider extends ContentProvider {

    private MyDatabase mydb;
    @Override
    public boolean onCreate() {
        // TODO Auto-generated method stub
        Context ctx = getContext();
        mydb = new MyDatabase(ctx);
        return (mydb == null) ? false : true;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // TODO Auto-generated method stub
         SQLiteDatabase db = mydb.getWritableDatabase();
         int token = MyContentDescriptor.URI_MATCHER.match(uri);
         int count=0;
        
         switch(token){
         case MyContentDescriptor.Categories.CAT_PATH_TOKEN:
            count= db.delete(MyContentDescriptor.Categories.TABLE_NAME, selection, selectionArgs);
             break;
         }
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
  
    }

    @Override
    public String getType(Uri uri) {
        // TODO Auto-generated method stub // returning self defined mime types
        // to be used by other applications if any
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // TODO Auto-generated method stub

        Log.d("sohail", "inside insert");
        SQLiteDatabase db = mydb.getWritableDatabase();

        int token = MyContentDescriptor.URI_MATCHER.match(uri);
        switch (token) {
        case MyContentDescriptor.Categories.CAT_PATH_TOKEN: // uri is of
                                                            // categories table
            Log.d("sohail", "matched uri is CAT_PATH_TOKEN:" + uri.toString());
            long id = db.insert(MyContentDescriptor.Categories.TABLE_NAME,
                    null, values);
            // notifying change to content observers
            getContext().getContentResolver().notifyChange(uri, null);
            return MyContentDescriptor.Categories.CONTENT_URI.buildUpon()
                    .appendPath(String.valueOf(id)).build();

        default:
            throw new UnsupportedOperationException("URI: " + uri
                    + " not supported.");
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        // TODO Auto-generated method stub
        Log.d("sohail", "query called");
        SQLiteDatabase db = mydb.getReadableDatabase();
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        Cursor c;
        int token = MyContentDescriptor.URI_MATCHER.match(uri);

        switch (token) {

        case MyContentDescriptor.Categories.CAT_PATH_TOKEN:
            Log.d("sohail", "matched uri is CAT_PATH_TOKEN:" + uri.toString());
            queryBuilder.setTables(MyContentDescriptor.Categories.TABLE_NAME);
            c = queryBuilder.query(db, projection, selection, selectionArgs,
                    null, null, sortOrder);
            return c;

        case MyContentDescriptor.Categories.CAT_PATH_FOR_ID_TOKEN:
            Log.d("sohail", "matched uri is CAT_PATH_TOKEN:" + uri.toString());
            queryBuilder.setTables(MyContentDescriptor.Categories.TABLE_NAME);
            queryBuilder.appendWhere(MyContentDescriptor.Categories.Cols.cat_id
                    + "=" + uri.getLastPathSegment());
            c = queryBuilder.query(db, projection, selection, selectionArgs,
                    null, null, sortOrder);
            return c;

       default:
            Log.d("sohail", "no URI MATCHED");
            return null;
        }

    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        // TODO Auto-generated method stub
         SQLiteDatabase db = mydb.getWritableDatabase();
         int token = MyContentDescriptor.URI_MATCHER.match(uri);
         int count=0;
        
         switch(token){
         case MyContentDescriptor.Categories.CAT_PATH_TOKEN:
            count= db.update(MyContentDescriptor.Categories.TABLE_NAME,values, selection, selectionArgs);
             break;
            }
        
        getContext().getContentResolver().notifyChange(uri, null);
        return count;
        
    }

}

Mycontentprovider is exented from contentprovider and its methods: Insert,query,delete,update getType needs to be implemented. We create a database db in onCreate. This object is used in insert,query update and delete methods. Uri matcher is used to match the requested URI and on the basis of this uri, query is executed on particular table. (there could be more than one table). db.insert, db.update and db.delete are all sqlite funcitons. SQLite queryBuilder is used to query the specific table as indicated in URI. Results of the query are returned in Cursor object (which in-turn is used to populate UI e.g listView). On each insertion, new id of the inserted row is appended in URI and returned e.g after insertion of 3rth record the returned URI will be:

     content://sohail.aziz.mycontentprovider/categories/3



this URI can be used to query the 3rd record (row) in categories table. Notice one thing that after insert, delete and update operation
getContext().getContentResolver().notifyChange(uri, null); is called. This notifies the content observers that the particular table has been modified. Thats all for the implementation of the contentprovider. Lets see how can we use it in our activity. Lets say we have two edit text and a check box named etID, etName and cbStatus.
private void InsertRecords() {
  // TODO Auto-generated method stub
  ContentValues conval=new ContentValues();
  conval.put(MyContentDescriptor.Categories.Cols.key_1_catid, etID.getText().toString() );
  conval.put(MyContentDescriptor.Categories.Cols.key_2_catname, etNAME.getText().toString());
  
  String stat;
  if(cbStatus.isChecked())
   stat="true";
  else
   stat="false";
  
  conval.put(MyContentDescriptor.Categories.Cols.key_3_catstatus,stat);
  recent_uri=getContentResolver().insert(MyContentDescriptor.Categories.CONTENT_URI, conval);
  Log.d("sohail","returned uri="+recent_uri);
  //showRecords();
  
 }
private void showRecords() {
  // TODO Auto-generated method stub
  
   cur= this.getContentResolver().query(MyContentDescriptor.Categories.CONTENT_URI, null, null,null, null);
  
  String[] colums=new String[]{MyContentDescriptor.Categories.Cols.key_1_catid,MyContentDescriptor.Categories.Cols.key_2_catname};
  //adapter= new SimpleCursorAdapter(this,android.l)
   int[] to = new int[] { R.id.tvID, R.id.tvNAME };
   
   adapter = new SimpleCursorAdapter(this, R.layout.list_item, cur, colums, to);

   listview.setAdapter(adapter);
  
   
 } 
private void deleteAll() {
  // TODO Auto-generated method stub
  getContentResolver().delete(MyContentDescriptor.Categories.CONTENT_URI, null, null);
 }
private void showRecent() {
  // TODO Auto-generated method stub
  Cursor cur2= this.getContentResolver().query(recent_uri, null, null,null, null);
  adapter.changeCursor(cur2);  
  
 }
private void showchecked() {
  // TODO Auto-generated method stub
  boolean status=true;
  String colname=MyContentDescriptor.Categories.Cols.key_3_catstatus;
  Uri uri= MyContentDescriptor.Categories.CONTENT_URI;
  String selection= MyContentDescriptor.Categories.Cols.key_3_catstatus+"=?";
  Cursor cur3=getContentResolver().query(uri, null,selection, new String[]{"true"}, null);
  Log.d("sohail",cur3.toString());
  
  adapter.changeCursor(cur3);
  
  
  } 

Functions Description:
Insert: to insert new record.
showRecords: to show all records of categories table.
showRecent: to show the recently inserted record.
showChecked:to show all records where status=true.
deleteAll: to delete all records from categories table.

Browse and download source
MyContentProviderExample.

*Update* : Content provider does not provide synchronization by default, which means there can be synchronization issues if content provider is accessed (insert/update) from many threads/apps at once. However Sqlite does provide the synchronization. You need not to do anything if your ContentProvider is backed by Sqlite database. For detail explanation read SQLite, ContentProviders, and Thread Safety.

3 comments:

  1. Thank you for your post. You should definitely checkout the iosched app from Google. I like the way they organize the code. I am sure this will be a good inspiration for you. I am still searching for a tutorial which addresses multiple tables and answers the question if one would use one or multiple SQLiteOpenHelper. Also, this raises the question if you want to synchronize local data with a server.
    No offense, but: Please re-read your sentences. You got a lot of typos or bad English.

    ReplyDelete
  2. Thanks for reading so critically :). I'd definitely look into iosched. I am still a learner of Android, and write what I think is helpful and useful for others. I'll update these things when I get better things.
    I am not native English writer, so bear me please :)

    ReplyDelete
  3. Hey, thanks a ton for this tutorial. I have been searching for a working Custom Content Provider for ages. I read dozens of blogs, examples, tutorials and what not. But this is the first example that actually worked without crashing application. Really appreciate this tutorial.
    Thanks a lot mate, you made my day !! :D

    ReplyDelete