A Pattern for Creating Custom Android Content Providers - No Fluff Just Stuff

A Pattern for Creating Custom Android Content Providers

Posted by: Vladimir Vivien on November 19, 2011

When creating apps in Android, you have access to numerous data sources. In fact, Android comes with a built-in instance of SQLite, provides access data to local storage, gives direct access to remote resources over HTTP. In additions to these native data providers, you can setup your own application to be a data provider. Since Android does not allow applications to share data directly with one another, you can make your data accessible to (components in your application or) other applications using the ContentAPI. For instance, Android has a phonebook content provider which lets you search, add, modify contacts.

A ContentProvider is a first-class Android component (like Activity, Service, etc). It is registered in the manifest file like any other high-level components. You can read about Android ContentProviders from Android’s website at:

The intent of this write up is to present a simple approach to create and work with your own ContentProvider. It is not meant to be an introduction to data storage in Android. If you have not used data storage in Android, visit the Android’s developer web site and look for data storage.

ContentProvider Overview

As mentioned above, one of the primary purposes for the use of ContentProvider is data sharing.  Since databases and other internal data stores are application-scoped, there’s no way to share information between applications.  A content provider lets you expose access to your application’s data in a structured and uniform manner.

ContentProdviders are considered data access objects (a software pattern).  Their backing data store can be a database, data from local storage, data from a remote server over HTTP, or a custom data source.  For this write up, I will use the SQLite database as the backing datastore for the sample content provider that will be be demonstrated.  This will keep things simple since the API for SQLite maps nicely to the method used by the ContentProvider API.

The Example

Imagine a simple application that keeps track of your favorite restaurants and save that information in the local SQLite database.  For the sake of keeping things simple, the code only saves a few columns about the restaurants including name, address, city, state, etc. You can download the example from its GitHub location at https://github.com/vladimirvivien/workbench/tree/master/android-tutorials/ContentProviderSample.

How to Do It

The ContentProvider API is a bit heavy and can be complex.  To get a custom ContentProvider written, you need to implement several boilerplate methods. This writeup makes things easier by providing a set of guidelines for implementing your own ContentProvider. Here is how to do it:
  • Define data model – figure out what will be in stored
  • Create a Descriptor class – to help describe the data that you will work with.  This is not part of any of the APIs.  It is a class that I have used to help with with creation of providers.
  • Create a Database Class – the database class is implemented as a SQLiteOpenHelper intended to help with creation/management of the database instance itself.
  • Define your ContentProvider – the content provider will use the classes created above to install the database, access, and manipulate the data in it.  This is also the class that Activity classes will use to access the data.

Define the Data Model

While it may sound trivial, the first step to this pattern is to figure out what your data will look like. Recall that the ContentProvider exposes data in tabular form, so listing out your data columns is a good first step for your design.  For our favorite restaurant example, we will use the following fields which represent the name and address of our favorite eateries:

  • ID
  • NAME
  • ADDRESS
  • CITY
  • ZIP

It’s good practice to define an ID column as an identifier for the data row. The ContentProvider’s URI mechanism uses the ID to refer to saved data entities.

Descriptor Class

The Descriptor class is not part of the ContentProvider API. I use it as a registry for meta data that describes the content exposed by the ContentProvider class. The Descriptor class manages the following meta data:
  • URI Authority – the authority portion of the URI representing this entity.  In our example it is “com.favrestaurant.contentprovider”
  • URI Matcher – this is an internal registry used to map a URI path (serviced by the ContentProvider) to an integer value.
  • Entity Class – an inner static class that represents the entity to be managed by the ContentProvider. In our example, this class is called Restaurant. It exposes meta data such as the entity name, supported URIs, etc.
  • Class EntityClass.Cols – the entity class provides an inner class called Cols. As you may have guessed, this class exposes the name of the columns to exposed by the ContentProvider for the entity.
public class ContentDescriptor {
	public static final String AUTHORITY = "demo.contentprovider.restaurant";
	private static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
	public static final UriMatcher URI_MATCHER = buildUriMatcher();

	private ContentDescriptor(){};

	private static  UriMatcher buildUriMatcher() {
        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
        final String authority = AUTHORITY;

        matcher.addURI(authority, Restaurant.PATH, Restaurant.PATH_TOKEN);
		matcher.addURI(authority, Restaurant.PATH_FOR_ID, Restaurant.PATH_FOR_ID_TOKEN);

        return matcher;
	}

	public static class Restaurant {
		public static final String NAME = "restaurant";

		public static final String PATH = "restaurants";
		public static final int PATH_TOKEN = 100;
		public static final String PATH_FOR_ID = "restaurants/*";
		public static final int PATH_FOR_ID_TOKEN = 200;

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

		public static final String CONTENT_TYPE_DIR = "vnd.android.cursor.dir/vnd.favrestaurant.app";
		public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.favrestaurant.app";

		public static class Cols {
			public static final String ID = BaseColumns._ID; // convention
			public static final String NAME = "restaurant_name";
			public static final String ADDRESS  = "restaurant_addr";
			public static final String CITY = "restaurant_city";
			public static final String STATE = "restaurant_state";
			public static final String ZIP = "restaurant_zip";
		}

	}
}

What’s going on…

  • The first thing to notice in the code above is the definition of variables AUTHORITY and BASE_URI. Together these form the URI that identifies the ContentProvider. The URI is used by Android for registering the ContentProvider as part of the application. As you will see later, a ContentResolver class will locate and use the ContentProvider based on the provided URI.
  • Private method buildMatcher() creates an instance of URIMatcher for the ContentProvider.
  • Inner class Restaurant exposes meta data that defines the Restaurant entity managed by the associated ContentProvider.
  • Furthermore, inner class Restaurant.Cols define meta values for the columns associated with the Restaurant entity.

If none of this makes sense, read on to see how the Descriptor class is used.

The Database Class (SQLiteOpenHelper)

Since the backing data store for our ContentProvider implementation is a database, we will use the SQLLite API here to define the database. The purpose of class RestaurantDatabase is to create, install, and help manage the SQLLite database. The Android’s ContentProvider API (along with the ContentResolver class) uses this class to run DDL scripts to install and update the database. If you implement the onUpgrade() method and change the version of the database, the database will be upgraded automatically next time the code is executed.

The one notable portion of the code below is its use of the ContentDescriptor class (see defined above) to provide meta data the table and fields used in the database.

public class RestaurantDatabase extends SQLiteOpenHelper {
	private static final String DATABASE_NAME = "fav_restaurnt.db";
	private static final int DATABASE_VERSION = 2;

	public RestaurantDatabase(Context ctx){
		super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL("CREATE TABLE " + ContentDescriptor.Restaurant.NAME+ " ( " +
				ContentDescriptor.Restaurant.Cols.ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
				ContentDescriptor.Restaurant.Cols.NAME + " TEXT NOT NULL, " +
				ContentDescriptor.Restaurant.Cols.ADDRESS 	+ " TEXT , " +
				ContentDescriptor.Restaurant.Cols.CITY + " TEXT, " +
				ContentDescriptor.Restaurant.Cols.STATE + " TEXT, " +
				ContentDescriptor.Restaurant.Cols.ZIP + " TEXT, " +
				"UNIQUE (" +
					ContentDescriptor.Restaurant.Cols.ID +
				") ON CONFLICT REPLACE)"
			);
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if(oldVersion < newVersion){
        	db.execSQL("DROP TABLE IF EXISTS " + ContentDescriptor.Restaurant.NAME);
        	onCreate(db);
        }
	}

}

The ContentProvider Class

Next, we implement the ContentProvider class with the logic for data access and update. The ContentProvider API exposes several methods for inserting, updating, querying, and deleting data. The example code only implements the insert() and query() methods. Note the usage of the ContentDescriptor to provide naming and configuration meta data for the ContentProvider.

public class RestaurantContentProvider extends ContentProvider {
	private RestaurantDatabase restaurantDb;

	@Override
	public boolean onCreate() {
		Context ctx = getContext();
		restaurantDb = new RestaurantDatabase(ctx);
		return true;
	}

	@Override
	public String getType(Uri uri) {
		final int match = ContentDescriptor.URI_MATCHER.match(uri);
		switch(match){
		case ContentDescriptor.Restaurant.PATH_TOKEN:
			return ContentDescriptor.Restaurant.CONTENT_TYPE_DIR;
		case ContentDescriptor.Restaurant.PATH_FOR_ID_TOKEN:
			return ContentDescriptor.Restaurant.CONTENT_ITEM_TYPE;
        default:
            throw new UnsupportedOperationException ("URI " + uri + " is not supported.");
		}
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		SQLiteDatabase db = restaurantDb.getWritableDatabase();
		int token = ContentDescriptor.URI_MATCHER.match(uri);
		switch(token){
			case ContentDescriptor.Restaurant.PATH_TOKEN:{
				long id = db.insert(ContentDescriptor.Restaurant.NAME, null, values);
				getContext().getContentResolver().notifyChange(uri, null);
				return ContentDescriptor.Restaurant.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) {
		SQLiteDatabase db = restaurantDb.getReadableDatabase();
		final int match = ContentDescriptor.URI_MATCHER.match(uri);
		switch(match){
			// retrieve restaurant list
			case ContentDescriptor.Restaurant.PATH_TOKEN:{
				SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
				builder.setTables(ContentDescriptor.Restaurant.NAME);
				return builder.query(db, null, null, null, null, null, null);
			}
			default: return null;
		}
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		return 0;
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		return 0;
	}
}

What’s going on…

  • The onCreate() method is called when the provider is instantiated (by the ContentResolver class). It is, in turn, used to bootstrap the database via the RestaurantDatabase class (see above) instance db.
  • The getType() method uses the ContentDescriptor.URI_MATCHER (see ContentDescriptor above) to lookup the MIME type for a given URI.
  • All of the data access & update methods (including query(), insert(), update(), and delete()) take a URI parameter. The URI provides hints such as the entity (and cardinality) being queried or updated. For instance, in our example, if the URI to passed to the query() method looks like content://com.favrestaurant.contentprovider/restaurants/* the method will return all restaurant rows in the database. This is accomplished by using the ContentDescriptor.URI_MATCHER to determine how to process the URI.

Using the ContentProvider

Once you have all of your pieces in place, you can access the data exposed by the content provider using the ContentResolver. There are certainly more robust ways to use to access data from a ContentProvier. This write up shows the simplest (non-production ready) way of doing it. You should investigate which way works best for your use (see http://developer.android.com/guide/topics/providers/content-providers.html).


public class FavRestaurantActivity extends Activity {
	TextView txtName;
	TextView txtAddr;
	TextView txtState;
	TextView txtCity;
	TextView txtZip;

    ContentResolver contentResolver;
    Cursor cur;
    SimpleCursorAdapter adapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ...
        contentResolver = this.getContentResolver();
    }

    @Override
    public void onStop() {
    	super.onStop();
    	if(cur != null) cur.close();
    }

    private void loadContent() {
        cur = this.getContentResolver().query(ContentDescriptor.Restaurant.CONTENT_URI, null, null, null, null);
    	...
    }

    private void saveContent(){
    	ContentValues val = new ContentValues();
    	val.put(ContentDescriptor.Restaurant.Cols.NAME, (this.txtName.getText() != null) ? this.txtName.getText().toString() : null);
    	val.put(ContentDescriptor.Restaurant.Cols.ADDRESS, (this.txtAddr.getText() != null) ? this.txtAddr.getText().toString() : null);
    	val.put(ContentDescriptor.Restaurant.Cols.CITY, (this.txtCity.getText() != null) ? this.txtCity.getText().toString() : null);
    	val.put(ContentDescriptor.Restaurant.Cols.STATE, (this.txtState.getText() != null) ? this.txtState.getText().toString() : null);
    	val.put(ContentDescriptor.Restaurant.Cols.ZIP, (this.txtZip.getText() != null) ? this.txtZip.getText().toString() : null);
    	contentResolver.insert(ContentDescriptor.Restaurant.CONTENT_URI, val);
    	loadContent();
    }
}

What is going on…

  • First, let me point out that the code used above to access data from the Activity class is not optimal for production. You should optimize any io-bound code that has propensity to hang the UI by using an asynchronous task. Nevertheless, the code presented above uses an instance of ContentResolver to access data managed by the backing ContentProvider. The ContentResolver uses the URI value passed in to select the proper ContentProvider registered in the AndroidManifest.xml file (not shown).
  • The saveContent() shows how you can use the Descriptor class to create an instance of ContentValues (from the ContentProvider API) to save data. Each column is mapped to its value using the ContentProvider to provide the column name.
  • Conclusion

    This write up provides a guide for those of you, brave enough, to use the ContentProvider API directly. I have introduced the Descriptor class as a container to register meta data to describe the data element captured by the ContentProvier. The hope is to make using the ContentProvider API more organized and provide some structure when putting your own data access code together.


Vladimir Vivien

About Vladimir Vivien

Vladimir Vivien is a software engineer living in the United States. Past and current experiences include development in Java and C#.Net for industries including publishing, financial, and healthcare. He has a wide range of technology interests including Java, OSGi, Groovy/Grails, JavaFX, SunSPOT, BugLabs, module/component-based development, and anything else that runs on the JVM.

Vladimir is the author of “JavaFX Application Development Cookbook” published by Packt Publishing. He is the creator of the Groovv JmxBuilder open source project, a JMX DSL, that is now part of the Groovy language. Other open source endeavor includes JmxLogger and GenShell. You can follow Vladimir through his blog: http://blog.vladimirvivien.com/, Twitter: http://twitter.com/vladimirvivien, and Linked In: http://www.linkedin.com/in/vvivien.

Why Attend the NFJS Tour?

  • » Cutting-Edge Technologies
  • » Agile Practices
  • » Peer Exchange

Current Topics:

  • Languages on the JVM: Scala, Groovy, Clojure
  • Enterprise Java
  • Core Java, Java 8
  • Agility
  • Testing: Geb, Spock, Easyb
  • REST
  • NoSQL: MongoDB, Cassandra
  • Hadoop
  • Spring 4
  • Cloud
  • Automation Tools: Gradle, Git, Jenkins, Sonar
  • HTML5, CSS3, AngularJS, jQuery, Usability
  • Mobile Apps - iPhone and Android
  • More...
Learn More »