Wednesday, January 13, 2010

Writing your own ContentProvider

Hey everyone,

So the following tutorial will hopefully give you a good idea of how to implement your own ContentProvider. I know that there are a lot of pretty good sites out there with some good code snippets, but I’ve noticed that not many really help the developer understand what’s going on step by step so my goal is to not only provide an example of a fully implemented ContentProvider but also to step you through the process.

So I’ll start by just posting all of the code, and I’ll go through it slowly afterwards. We’ll start with the Custom Content Provider itself:

package jason.wei.apps.securenotes.providers; import jason.wei.apps.securenotes.db.Note.Notes; import java.util.HashMap; 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.util.Log; /** * @author Jason Wei * */ public class NotesContentProvider extends ContentProvider { private static final String TAG = "NotesContentProvider"; private static final String DATABASE_NAME = "notes.db"; private static final int DATABASE_VERSION = 1; private static final String NOTES_TABLE_NAME = "notes"; public static final String AUTHORITY = "jason.wei.apps.notes.providers.NotesContentProvider"; private static final UriMatcher sUriMatcher; private static final int NOTES = 1; private static HashMap notesProjectionMap; 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.NOTE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + Notes.TITLE + " VARCHAR(255)," + Notes.TEXT + " LONGTEXT" + ");"); } @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_TABLE_NAME); onCreate(db); } } private DatabaseHelper dbHelper; @Override public int delete(Uri uri, String where, String[] whereArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case NOTES: count = db.delete(NOTES_TABLE_NAME, where, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case NOTES: return Notes.CONTENT_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } @Override public Uri insert(Uri uri, ContentValues initialValues) { if (sUriMatcher.match(uri) != NOTES) { throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } SQLiteDatabase db = dbHelper.getWritableDatabase(); long rowId = db.insert(NOTES_TABLE_NAME, Notes.TEXT, values); if (rowId > 0) { Uri noteUri = ContentUris.withAppendedId(Notes.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(noteUri, null); return noteUri; } throw new SQLException("Failed to insert row into " + uri); } @Override public boolean onCreate() { dbHelper = new DatabaseHelper(getContext()); return true; } @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(notesProjectionMap); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case NOTES: count = db.update(NOTES_TABLE_NAME, values, where, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(AUTHORITY, NOTES_TABLE_NAME, NOTES); notesProjectionMap = new HashMap(); notesProjectionMap.put(Notes.NOTE_ID, Notes.NOTE_ID); notesProjectionMap.put(Notes.TITLE, Notes.TITLE); notesProjectionMap.put(Notes.TEXT, Notes.TEXT); } }

And next a little helper class that keeps all of the columns organized and readily accessible:

public class Note { public Note() { } public static final class Notes implements BaseColumns { private Notes() { } public static final Uri CONTENT_URI = Uri.parse("content://" + NotesContentProvider.AUTHORITY + "/notes"); public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.jwei512.notes"; public static final String NOTE_ID = "_id"; public static final String TITLE = "title"; public static final String TEXT = "text"; } }

(Note that this is similar to how Google provides you with the columns, i.e. People._ID, People.NUMBER, CallLog.CACHED_NAME, etc)

Okay so let’s go through this step by step. First, when you extend ContentResolver, there are 6 methods you need to overwrite:

query()

insert()

update()

delete()

getType()

onCreate()

Normally these are just wrapper functions around the raw SQL queries. For instance, the method:

db.delete(tableName, where, whereArgs);

Is simply just a wrapper around the SQL query that looks something like:

"delete from " + tableName + " where " + where + " ? " + whereArgs"

So for those who know SQL, if I wanted to delete the note with title “Hello World” then my queries would look like:

// wrapper query db.delete(NOTES_TABLE_NAME, Notes.TITLE + "= ' " + "Hello World" + " ' ", null); // real query when translated String query = "delete from notes where title = 'Hello World' ";

And so as you look at how I overwrite the 6 methods, you’ll see that I’m simply taking in the parameters and inputting them appropriately into the wrapper methods. Of course, you can override them as you like depending on what you want your application to do. Perhaps your application is ONLY worried about retrieving the average of some numbers. Then in your query() method, maybe instead of querying for the numbers normally, and then always having to iterate through the numbers and find the average from the JAVA side, you could customize your query to automatically return the average BY DEFAULT.

But in case you’re still confused, let’s look at an example:

public int delete(Uri uri, String where, String[] whereArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case NOTES: count = db.delete(NOTES_TABLE_NAME, where, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; }

So basically you pass in the Uri (in our case it will be Notes.CONTENT_URI to tell the system which table you’re going to target) and if the Uri matches NOTES then we use our SQLiteDatabase (retrieved in the first line) to delete from the notes table using the where arguments passed in.

A similar idea goes into overriding query(), insert(), and update(), and in our onCreate() method we are simply using our SQLiteDatabase to execute a CREATE TABLE statement where we build our table using our defined columns (i.e. Notes.NOTE_ID, Notes.TITLE, and Notes.TEXT) and also make sure we use the proper SQLite3 column types.

One thing to note here is that Android requires that the unique id column have name “_id”.

This is extremely important as otherwise your content provider will not get registered properly.

Now, one last thing before I get to how you register it in your Manifest is what this AUTHORITY String is all about. In my case it was:

public static final String AUTHORITY = "jason.wei.apps.notes.providers.NotesContentProvider";

So the authority you can more or less define in anyway that you like, but typically it will look something like:

{package name}.providers.{Custom Provider name}

For organizational purposes, this means that in your Android project, you will have to create a new package called:

{package name}.providers

In order for the Uri path to be correct (again pretty obvious, but who knows).

And so this AUTHORITY basically just helps you define the CONTENT_URI of your ContentProvider (i.e. the path to your database), and you will register your provider in your Manifest like so:

And so you see that I define my content providers in a way such that the project path is equal to the authorities path (again, not required, but probably recommended for simplicity and organizational issues).

And that’s it! Now you have a fully registered and created ContentProvider. My next example/tutorial will show you how to write cool wrapper functions that help you make queries and manipulate data in whatever way you want!

Let me know if you have questions, otherwise Happy Coding.

- jwei

[Via http://thinkandroid.wordpress.com]

No comments:

Post a Comment