Header Ads

Building Android Content Providers

Content providers are the way that Android applications can share info between each other. an application can ask for info from another application using content providers.

In this post we're going to create a content provider to access data from our previous Employees simple application from the SQLite post.

to remind you the database has two tables Employees and Dept

remember that any content provider must provide the following:
  1. A URi from which we can run queries.
  2. MIME type corresponding to the content.
  3. Insert() method.
  4. Update() methd.
  5. Delete() method.

Creating the content type:

first we will create a new class, I will call it EmployeesContentProvider and choose its super class to be ContentProvider. the class initially will be like this:
package mina.android.DatabaseDemo;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

public class EmployeesContentProvider extends ContentProvider {
DatabaseHelper db;
public static final Uri CONTENT_URI=Uri.parse("content://employees");
@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
// TODO Auto-generated method stub
return 0;
}

@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}

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

@Override
public boolean onCreate() {
// TODO Auto-generated method stub
return false;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO Auto-generated method stub
return null;
}

@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}

}


we added a member of type DatabaseHelper db to hold a reference to our database.
also we added a static URi object that represents the URi of our content provider.
it has all the abstract methods implementations of the ContentProvider class. so let's check each method

onCreate() method:

the onCreate() method is the first method invoked when the content provider is created (similar to the Activity's onCreate() method). here you can load your database or check for files you may read/write to them.
the method return a Boolean. it should be true if everything is ok, otherwise it should be false.
in our case we will just reference our SQLite database:
@Override
public boolean onCreate() {
// TODO Auto-generated method stub
db=new DatabaseHelper(this.getContext());
if(db==null)
return false;
else
return true;
}

query() method:

the query() method is the method that gets invoked when a content provider data is requested by a URi.

but first let's talk a little about content providers URI.
the content provider URi has the following format:
content://Authority/[(n) path]/[instance indentifier]
explanation:
  • the URI starts with content:// scheme.
  • the authority is a unique identifier for the content provider.
  • the authority can be followed by one or more paths (optional) refer to data paths within the content.
  • there can be an instance identifier that refers to a specific data instance.
for example we can have a URi like this content://Employees/Marketing//11.
this URi has Employees as the authority, Marketing as a data path and 11 as an instance (employee) identifier.

back to our query method, we have the following parameters:
  1. Uri: the URi requested.
  2. String [] projection: representing the columns (projection) to be retrieved.
  3. String[] selection: the columns to be included in the WHERE clause.
  4. String[] selectionArgs: the values of the selection columns.
  5. String sortOrder: the ORDER BY statement.
the first step in our query method is to parse the client URi.
we expect the URi to be in one of the following forms:
  1. content://employees/: retrieves all employees.
  2. content://employees/id: retrieves a certain employee by ID.
  3. content://employess/IT: retreives employees of IT Dept.
  4. content://employess/HR: retrieves employees of HR Dept.
  5. content://employees/Sales: retreives employees of sales Dept.
so we will add some constatnt values to our class to refer to the above URis:
//authority and paths
public static final String AUTHORITY="employees";
public static final String ITPATH="IT";
public static final String HRPATH="HR";
public static final String SALESPATH="Sales";


//URiMatcher to match client URis
public static final int ALLEMPLOYEES=1;
public static final int SINGLEEMPLOYEE=2;
public static final int IT=3;
public static final int HR=4;
public static final int SALES=5;
then we're going to define a URiMatcher object that matches the client URi
static final UriMatcher matcher=new UriMatcher(UriMatcher.NO_MATCH);
static{
matcher.addURI(AUTHORITY,null,ALLEMPLOYEES);
matcher.addURI(AUTHORITY, ITPATH, IT);
matcher.addURI(AUTHORITY, HRPATH, HR);
matcher.addURI(AUTHORITY, SALESPATH, SALES);
//you can use '*' as a wild card for any text
matcher.addURI(AUTHORITY, "#", SINGLEEMPLOYEE);
}
the static initializer block loads the URimatcher objects with the values to match when the class initializes.
so let's write our query method:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder builder=new SQLiteQueryBuilder();

builder.setTables(DatabaseHelper.viewEmps);

String order=null;
Cursor result=null;
if(sortOrder!=null)
order=sortOrder;
int match=matcher.match(uri);
switch(match)
{
case ALLEMPLOYEES:
//content://employees//id
result=builder.query(db.getWritableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);
break;
case SINGLEEMPLOYEE:
//content://employees//id
Listsegments=uri.getPathSegments();
String empID=segments.get(0);
result=db.getEmpByID(empID);

break;
case IT:
//content://employees//IT
result=db.getEmpByDept("IT");
result=builder.query(db.getReadableDatabase(), projection, db.colDeptName+"=?", new String[]{"IT"}, null, null, sortOrder);
break;
case HR:
//content://employees//HR
result=db.getEmpByDept("HR");
result=builder.query(db.getReadableDatabase(), projection, db.colDeptName+"=?", new String[]{"HR"}, null, null, sortOrder);
break;
case SALES:
//content://employees//Sales
result=db.getEmpByDept("Sales");
result=builder.query(db.getReadableDatabase(), projection, db.colDeptName+"=?", new String[]{"Sales"}, null, null, sortOrder);

break;

}

return result;
}

the function just parses the URi and returns the data in a cursor.

Insert() method:
the insert methods inserts a new record to the db;
the insert method has the following form:
public Uri insert(Uri uri, ContentValues values) {

return null;
}
the method has two parameters:
  1. URi uri: the URi of the content provider, we need to check it's correct.
  2. ContentValues values: object holding the info of the new item to be inserted.
the method returns the URi of the newly inserted item to be used for further manipulations.
so here's the implentation:
@Override
public Uri insert(Uri uri, ContentValues values) {
int match=matcher.match(uri);
//not the Uri we're expecting
long newID=0;
if(match!=1)
throw new IllegalArgumentException("Wrong URi "+uri.toString());
if(values!=null)
{
newID=db.getWritableDatabase().insert(DatabaseHelper.employeeTable, DatabaseHelper.colName, values);
return Uri.withAppendedPath(uri, String.valueOf(newID));

}
else
return null;
}
we first check the Uri if it is not correct, throw an exception.
then check the content values object, if null return null otherwise insert the new item and return the URi with the id of the new item.
the Update() method:
the update method updates existing record(s) and returns the number of updated rows.
a trick rises from the fact that you need to specify whether to update a collection of records or a single record, based on the URi.
the method has the following parameters:
  1. URi uri: the URi of the content provider, we need to check it's correct.
  2. ContentValues values: object holding the info of the new item to be inserted.
  3. String Selection : the filter to match the rows to update
  4. String [] selectionArgs : the values of the filter parameters
here's the implementation of the update method:
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int match=matcher.match(uri);
//not the Uri we're expecting
int rows=0;
//update single instance
if(match==2)
{
if(values!=null)
{
Listsegments=uri.getPathSegments();
String empID=segments.get(0);
rows=db.getWritableDatabase().update(DatabaseHelper.employeeTable, values,DatabaseHelper.colID+"=?", new String []{empID});

}

}
//update all emps in a certain dept
else if(match==3 ||match==4||match==5)
{
Listsegments=uri.getPathSegments();
String deptName=segments.get(0);
int DeptID=db.GetDeptID(deptName);
rows=db.getWritableDatabase().update(db.employeeTable, values,db.colDept+"=?", new String []{String.valueOf(DeptID)});

}
return rows;
}
the Delete() method:
the delete method has the following parameters:
  1. Uri uri: the URi of the content provider.
  2. String Condition: the condition of the delete statement.
  3. String[] args: the delete condition arguments

so here's the implementation:
@Override
public int delete(Uri uri, String where, String[] args) {

int match=matcher.match(uri);
//expecting the URi to be in the form of
if(match==1)
{
SQLiteDatabase dataBase=db.getWritableDatabase();
return dataBase.delete(db.employeeTable, where, args);
}
else
return 0;
}
we just check for the URi and perform a delete command.
The getType() method:
the last mthod to implement is getType() method which returns the MIME type associated with the URi passed to it.
if the URi is of a group of employees, then the MIME type is a collection type, otherwise it's of an instance type
@Override
public String getType(Uri uri) {
int match=matcher.match(uri);
// single employee
if(match==2)
{
return "mina.android.Employee";
}
//collection of employees
else
{
return "mina.android.Employees";
}
}
Modifying the Manifest.xml file:
the last thing we need to do is to add an entry in our application's manifest.xml file to register our class as a content provider class.
so add this entry just below the <application>
<provider android:name="mina.android.DatabaseDemo.EmployeesContentProvider" 
android:authorities="employees"/>

the
when an application requests data through our content provider, Android system will search all the manifest files of all aplications on the device and when it finds such an entry, it will process the request

Testing the content provider:
now suppose you are in another activity and you want to use our activity.
Testing queries:
to make a query to retrieve all employees:
Uri empsUri=Uri.parse("content://employees");
Cursor cursor=getContentResolver().query(empsUri, null, null, null, null);Cursor cursor=getContentResolver().query(empsUri, null, null, null, null);
the cursor should have all the records.
to retrieve a certain employee by ID or all employees in a certain department:
Uri empUri=Uri.parse("content://employees//5");
Uri empDeptUri=Uri.parse("content://employees//Sales");

Inserting:
Uri empsUri=Uri.parse("content://employees");
ContentValues cvs=new ContentValues();
cvs.put("EmployeeName", "Mark Anderson");
cvs.put("Age", 35);
cvs.put("Dept", 1);
// URi of the new inserted item
Uri newEmp=getContentResolver().insert(empsUri, cvs);

Updating:
to update a single employee:
//Uri with the id of the employee
Uri empsUri=Uri.parse("content://employees/8");

Cursor cursor=getContentResolver().query(empsUri, null, null, null, null);
txt.setText(String.valueOf(cursor.getCount()));

ContentValues cvs=new ContentValues();
cvs.put("EmployeeName", "Mina Samy mod");
cvs.put("Age", 35);
cvs.put("Dept", 1);
// number of rows modified
int rowsNumber=getContentResolver().update(empsUri, cvs, "EmployeeID=?", new String[]{"8"});
to update all employees in a certain department
Uri empsUri=Uri.parse("content://employees/Sales");

Cursor cursor=getContentResolver().query(empsUri, null, null, null, null);
txt.setText(String.valueOf(cursor.getCount()));

ContentValues cvs=new ContentValues();
cvs.put("EmployeeName", "mod");
cvs.put("Age", 35);
cvs.put("Dept", 1);
int rowsNumber=getContentResolver().update(empsUri, cvs, "colDept=?", new String[]{"1"});

as a matter of fact, in both cases we don't need to specify the wher clause and the where parameters as they are implicitly specified in the URi. so we just can replace the update statement to be like this:
int rowsNumber=getContentResolver().update(empsUri, cvs, null,null);

Deleting:
I left the delete operation open to any criteria, you can delete a single employee or employees of a certain department or even all employees
Uri empsUri=Uri.parse("content://employees");
// delete employee of id 8
int rowsNumber=getContentResolver().delete(empsUri,"EmployeeID=?",new String[]{"8"});

Final Word:
creating a content provider for a certain type of data can be done in many ways, this example can be implemented in several variations.

another thing is that you need to create a strongly typed class for you data model to be used by clients accessing your content. in this example when I tested the query i wrote the column names of the database as strings like this: "EmployeeID" and "EmployeeName".
this is not ideal in a production release of an application. I should created a class library that holds all the info about the database to be used by other client applications.

you can download the source code for this example from here.

No comments:

Powered by Blogger.