Custom ContentProvider for Android – Part 2

Part 1
Part 3

In the previous part I talked about Contract Class and how to create it.
Today we will learn what is ContentProvider and what it consists of, and also write one.

As you may remember, we will request the DB using Content URI, which has information about particular ContentProvider and the database table. Here is one of ContentURI examples:

content://org.nerdgrl.examples.contentproviderexample.provider.ContractClass/students

Here we can see data scheme (content://), provider’s unique ID – authority (org.nerdgrl.examples.contentproviderexample.provider.ContractClass), and table’s name (students).

There is a class in Android SDK named URIMatcher, which stores mapping between Uri and some integer value. This value can be used in ‘switch’ operator to describe behavior for each Content URI.

Let’s setup four constant values for each of DB request types:

private static final int STUDENTS = 1;
private static final int STUDENTS_ID = 2;
private static final int CLASSES = 3;
private static final int CLASSES_ID = 4;

Then define UriMatcher value:

private static final UriMatcher sUriMatcher;

And setup it in ‘static’ block:

static {
	sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "students", STUDENTS);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "students/#", STUDENTS_ID);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "classes", CLASSES);
	sUriMatcher.addURI(ContractClass.AUTHORITY, "classes/#", CLASSES_ID);
}

We also should set projections for column selection in a request, it will be helpful in query() method:

private static HashMap sStudentsProjectionMap;
private static HashMap sClassesProjectionMap;

Let’s set all columns for the projection as a default value:

static {
	for(int i=0; i < ContractClass.Students.DEFAULT_PROJECTION.length; i++) {
		sStudentsProjectionMap.put(
			ContractClass.Students.DEFAULT_PROJECTION[i],
			ContractClass.Students.DEFAULT_PROJECTION[i]);
	}
	sClassesProjectionMap = new HashMap();
	for(int i=0; i < ContractClass.Classes.DEFAULT_PROJECTION.length; i++) {
		sClassesProjectionMap.put(
			ContractClass.Classes.DEFAULT_PROJECTION[i],
			ContractClass.Classes.DEFAULT_PROJECTION[i]);
	}
}

Now we need to define DBHelper class, which we will use to create DB and make requests to it via ContentProvider. Let's set DB's name, table names, column names, and also DB requests and update procedures:

private static class DatabaseHelper extends SQLiteOpenHelper {
	private static final String DATABASE_NAME = "ContractClassDB";
	
	public static final String DATABASE_TABLE_STUDENTS = ContractClass.Students.TABLE_NAME;
	public static final String DATABASE_TABLE_CLASSES = ContractClass.Classes.TABLE_NAME;
	
	public static final String KEY_ROWID  = "_id";
	public static final String KEY_FIRST_NAME   = "first_name";
	public static final String KEY_SECOND_NAME   = "second_name";
	public static final String KEY_CLASS_LETTER   = "class_letter";
	public static final String KEY_FK_CLASS_ID   = "fk_class_id";
	public static final String KEY_AVERAGE_SCORE   = "average_score";
	public static final String KEY_CLASS_NUMBER   = "class_number";
	
	private static final String DATABASE_CREATE_TABLE_STUDENTS =
		"create table "+ DATABASE_TABLE_STUDENTS + " ("
			+ KEY_ROWID + " integer primary key autoincrement, "
			+ KEY_FIRST_NAME + " string , "
			+ KEY_SECOND_NAME + " string , "
			+ KEY_AVERAGE_SCORE + " real , "
			+ KEY_FK_CLASS_ID + " integer, "
			+" foreign key ("+KEY_FK_CLASS_ID+") references "+DATABASE_TABLE_CLASSES+"("+KEY_ROWID+"));";
			
	private static final String DATABASE_CREATE_TABLE_CLASSES =
		"create table "+ DATABASE_TABLE_CLASSES + " ("
			+ KEY_ROWID + " integer primary key autoincrement, "
			+ KEY_CLASS_NUMBER + " string , "
			+ KEY_CLASS_LETTER + " string );";
			
	private Context ctx;
	DatabaseHelper(Context context) {
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
		ctx = context;
	}
	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL(DATABASE_CREATE_TABLE_STUDENTS);
		db.execSQL(DATABASE_CREATE_TABLE_CLASSES);
	}
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		
		db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE_STUDENTS);
		db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE_CLASSES);
		onCreate(db);
	}
}

More details about ContentProvider's methods:

onCreate() - initializes ContentProvider. Provider will be created as soon as you make your first request to it via ContentResolver
query() - queries DB and gets data, returns it in a Cursor
insert() - add new data to the DB and returns uri of a new row(-s)
update() - updates rows in the DB accordingly to selection
delete() - deletes rows
getType() - returns MIME type for a content URI

You should remember that all mentioned methods (except onCreate()) could be run in multiple threads and therefore must be thread-safe.

Now create our DBHelper in onCreate() method:

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

In the getType() method we just return data type from our ContractClass:

@Override
public String getType(Uri uri) {
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			return ContractClass.Students.CONTENT_TYPE;
		case STUDENTS_ID:
			return ContractClass.Students.CONTENT_ITEM_TYPE;
		case CLASSES:
			return ContractClass.Classes.CONTENT_TYPE;
		case CLASSES_ID:
			return ContractClass.Classes.CONTENT_ITEM_TYPE;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
	}
}

Let's look more closely to insert() method.

You can add rows only to tables, so we need to define a filter for unnecessary Content Uris:

if (sUriMatcher.match(uri) != STUDENTS &&
			sUriMatcher.match(uri) != CLASSES) {
	throw new IllegalArgumentException("Unknown URI " + uri);
}

Next, we get a DB object and create a structure for storing new row's data:

SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values;
if (initialValues != null) {
	values = new ContentValues(initialValues);
}
else {
	values = new ContentValues();
}

And choose where we should add this data:

switch (sUriMatcher.match(uri)) {
	case STUDENTS:
		rowId = db.insert(ContractClass.Students.TABLE_NAME,
			ContractClass.Students.COLUMN_NAME_FIRST_NAME,
			values);
		if (rowId > 0) {
			rowUri = ContentUris.withAppendedId(ContractClass.Students.CONTENT_ID_URI_BASE, rowId);
			getContext().getContentResolver().notifyChange(rowUri, null);
		}
	break;
	case CLASSES:
		...
	break;
}

There is an important line:

getContext().getContentResolver().notifyChange(rowUri, null);

It's responsible for updating our data in CursorAdapter (and, also in the ListView where it is used).

Full code for the method:

@Override
public Uri insert(Uri uri, ContentValues initialValues) {
	if (
		sUriMatcher.match(uri) != STUDENTS &&
		sUriMatcher.match(uri) != CLASSES
	) {
		throw new IllegalArgumentException("Unknown URI " + uri);
	}
	SQLiteDatabase db = dbHelper.getWritableDatabase();
	ContentValues values;
	if (initialValues != null) {
		values = new ContentValues(initialValues);
	}
	else {
		values = new ContentValues();
	}
	long rowId = -1;
	Uri rowUri = Uri.EMPTY;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			rowId = db.insert(ContractClass.Students.TABLE_NAME,
				ContractClass.Students.COLUMN_NAME_FIRST_NAME,
				values);
			if (rowId > 0) {
				rowUri = ContentUris.withAppendedId(ContractClass.Students.CONTENT_ID_URI_BASE, rowId);
				getContext().getContentResolver().notifyChange(rowUri, null);
			}
		break;
		case CLASSES:
			rowId = db.insert(ContractClass.Classes.TABLE_NAME,
				ContractClass.Classes.COLUMN_NAME_CLASS_NUMBER,
				values);
			if (rowId > 0) {
				rowUri = ContentUris.withAppendedId(ContractClass.Classes.CONTENT_ID_URI_BASE, rowId);
				getContext().getContentResolver().notifyChange(rowUri, null);
			}
		break;
	}
	return rowUri;
}

Now move to query() method:

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
	SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
	String orderBy = null;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			qb.setTables(ContractClass.Students.TABLE_NAME);
			qb.setProjectionMap(sStudentsProjectionMap);
			orderBy = ContractClass.Students.DEFAULT_SORT_ORDER;
			break;
		case STUDENTS_ID:
			qb.setTables(ContractClass.Students.TABLE_NAME);
			qb.setProjectionMap(sStudentsProjectionMap);
			qb.appendWhere(ContractClass.Students._ID + "=" + uri.getPathSegments().get(ContractClass.Students.STUDENTS_ID_PATH_POSITION));
			orderBy = ContractClass.Students.DEFAULT_SORT_ORDER;
			break;
		case CLASSES:
			qb.setTables(ContractClass.Classes.TABLE_NAME);
			qb.setProjectionMap(sClassesProjectionMap);
			orderBy = ContractClass.Classes.DEFAULT_SORT_ORDER;
			break;
		case CLASSES_ID:
			qb.setTables(ContractClass.Classes.TABLE_NAME);
			qb.setProjectionMap(sClassesProjectionMap);
			qb.appendWhere(ContractClass.Classes._ID + "=" + uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION));
			orderBy = ContractClass.Classes.DEFAULT_SORT_ORDER;
			break;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
	}
	SQLiteDatabase db = dbHelper.getReadableDatabase();
	Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
	c.setNotificationUri(getContext().getContentResolver(), uri);
	return c;
}

Here we use SQLiteQueryBuilder object to construct a query. Methods named setTables() and setProjectionMap() sets DB table and set of columns for a projection. To query a particular row method appendWhere() is used:

qb.appendWhere(ContractClass.Classes._ID + "=" + uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION));

which adds WHERE clause to the request. You could notice that we use ContractClass.Classes.CLASSES_ID_PATH_POSITION (in this case, for Classes table) - this is how we define position of the row's ID in the ContentUri (content://<authority>/classes/1)

update() method has similar structure:

@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
	SQLiteDatabase db = dbHelper.getWritableDatabase();
	int count;
	String finalWhere;
	String id;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			count = db.update(ContractClass.Students.TABLE_NAME, values, where, whereArgs);
			break;
		case STUDENTS_ID:
			id = uri.getPathSegments().get(ContractClass.Students.STUDENTS_ID_PATH_POSITION);
			finalWhere = ContractClass.Students._ID + " = " + id;
			if (where !=null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.update(ContractClass.Students.TABLE_NAME, values, finalWhere, whereArgs);
			break;
		case CLASSES:
			count = db.update(ContractClass.Classes.TABLE_NAME, values, where, whereArgs);
			break;
		case CLASSES_ID:
			id = uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION);
			finalWhere = ContractClass.Classes._ID + " = " + id;
			if (where !=null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.update(ContractClass.Classes.TABLE_NAME, values, finalWhere, whereArgs);
			break;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
	}
	getContext().getContentResolver().notifyChange(uri, null);
	return count;
}

Take note about this part:

finalWhere = ContractClass.Students._ID + " = " + id;
if (where !=null) {
	finalWhere = finalWhere + " AND " + where;
}

Here we add WHERE clause, which defines that we need only row with particular ID.

Code for delete() methos:

@Override
public int delete(Uri uri, String where, String[] whereArgs) {
	SQLiteDatabase db = dbHelper.getWritableDatabase();
	String finalWhere;
	int count;
	switch (sUriMatcher.match(uri)) {
		case STUDENTS:
			count = db.delete(ContractClass.Students.TABLE_NAME,where,whereArgs);
			break;
		case STUDENTS_ID:
			finalWhere = ContractClass.Students._ID + " = " + uri.getPathSegments().get(ContractClass.Students.STUDENTS_ID_PATH_POSITION);
			if (where != null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.delete(ContractClass.Students.TABLE_NAME,finalWhere,whereArgs);
			break;
		case CLASSES:
			count = db.delete(ContractClass.Classes.TABLE_NAME,where,whereArgs);
			break;
		case CLASSES_ID:
			finalWhere = ContractClass.Classes._ID + " = " + uri.getPathSegments().get(ContractClass.Classes.CLASSES_ID_PATH_POSITION);
			if (where != null) {
				finalWhere = finalWhere + " AND " + where;
			}
			count = db.delete(ContractClass.Classes.TABLE_NAME,finalWhere,whereArgs);
			break;
	default:
		throw new IllegalArgumentException("Unknown URI " + uri);
	}
	getContext().getContentResolver().notifyChange(uri, null);
	return count;
}

In the next part I'll tell you how to use ContentProvider linked with CursorLoader. Full source code you can find here.

Leave a Reply

Your email address will not be published. Required fields are marked *